DEV Community

Cover image for Utility Type: KeyHasProperties
teamradhq
teamradhq

Posted on • Edited on

Utility Type: KeyHasProperties

A utility type creating a union of every key of type that has a specific property:

export type KeysHaveProperty<T, Prop extends string> = {
  [K in keyof T]: Prop extends keyof T[K] ? K : never;
}[keyof T];
Enter fullscreen mode Exit fullscreen mode

Simply provide a type and a property name and we get all keys of that type that have a property with that name.

type ProductStore = {
  clothing: Store<HasVariation>;
  sporting: Store<HasVariation>;
  entertainment: Store<NoVariation>;
}

type ProductKeyWithVariation = KeysHaveProperty<
  ProductStore, 
  'variation'
>; // 'clothing' | 'sporting'
Enter fullscreen mode Exit fullscreen mode

Example: Dynamic Redux Selectors

Recently, I was working with a Redux store which had a number of collections whose state included:

type CollectionState<Item> = {
  all: Item[];
  current: Item;
};
Enter fullscreen mode Exit fullscreen mode

I wanted to define a dynamic selector that I could provide a store slice name and get all of the collection items like this:

const items = useAppSelector(selectAllItems('cart'));
Enter fullscreen mode Exit fullscreen mode

Defining the selector is relatively simple:

function selectAllItems<K extends keyof RootState>(collection: K) {
  return (state: RootState): RootState[K]['all'] => {
    state[collection].all
  };
} 
Enter fullscreen mode Exit fullscreen mode

This works just fine if every slice in your store has an all property. But it will show a lot of errors if any slice doesn't have it:

type RootState = {
  products: CollectionState<Product>;
  cart: CollectionState<CartItem> & CartSummary;
  settings: Record<'app' | 'checkout' | 'email', Preferences>
}
Enter fullscreen mode Exit fullscreen mode

This RootState will show a number of errors, and they can feel quite cryptic. But they are all occurring for the because RootState['settings'] doesn't have an all property.

TS2355: A function whose declared type is neither undefined, 
        void, nor any must return a value.
Enter fullscreen mode Exit fullscreen mode

Looking at the return value of RootState[K]['all'] it can feel confusing because we expect the all method to return a value. However, RootState['settings'] doesn't have an all property. If we call state.settings.all it evaluates to undefined.

TS2536: Type 'all' cannot be used to index type | RootState[K]
Enter fullscreen mode Exit fullscreen mode

For the same reason, RootState[K]['all'] is an invalid type because the property doesn't exist on all properties of RootState. If cart, products and settings all had a property called all then this wouldn't be a problem.

TS2339: Property all does not exist on type 
| RootState['products'] 
| RootState['cart']  
| RootState['settings']
Enter fullscreen mode Exit fullscreen mode

And finally, state[collection].all has this error because... wait for it... the all property doesn't exist on all properties of RootState.

Aside from these glaring issues, it won't complain if you call selectAllItems('settings') because keyof RootState is the valid type.

This is a problem because it's really the only TypeScript error we really want to see. We shouldn't be allowed to call selectAllItems('settings')because settings doesn't have an all property.

This is where this utility comes in handy:

function selectAllItems<
  Collection extends KeysHaveProperty<RootState, 'all'>
>(collection: Collection) {
  return (state: RootState): RootState[Collection]['all'] => {
    state[collection].all
  };
} 
Enter fullscreen mode Exit fullscreen mode

Now we are warned when we pass an invalid key name, and the correct return type is inferred:

useAppSelector(selectAllItems('settings')); // TS error
useAppSelector(selectAllItems('cart'));     // CartItem[]
useAppSelector(selectAllItems('products')); // Product[]
Enter fullscreen mode Exit fullscreen mode

Breaking it down

This utility is a mapped type that returns all keys of the type that have a property with a certain name.

export type KeysHaveProperty<T, Prop extends string> = {
  [K in keyof T]: Prop extends keyof T[K] ? K : never;
}[keyof T];
Enter fullscreen mode Exit fullscreen mode
  • We accept type (T) and property (Prop) generics.
  • We iterate over the keys (K) of T ([K in keyof T]).
  • We check if T[K] has Prop.
  • We return K if T[K]has`Prop.
  • We return never if T[K] doesn't have Prop.
  • Finally we declare the resulting type [keyof T] to get a union of all the keys that have the Prop.

Hopefully you find this useful :)

Top comments (0)