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];
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'
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;
};
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'));
Defining the selector is relatively simple:
function selectAllItems<K extends keyof RootState>(collection: K) {
return (state: RootState): RootState[K]['all'] => {
state[collection].all
};
}
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>
}
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.
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]
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']
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
};
}
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[]
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];
- We accept type (
T) and property (Prop) generics. - We iterate over the keys (
K) ofT([K in keyof T]). - We check if
T[K]hasProp. - We return
KifT[K]has`Prop. - We return
neverifT[K]doesn't haveProp. - Finally we declare the resulting type
[keyof T]to get a union of all the keys that have theProp.
Hopefully you find this useful :)
Top comments (0)