Utility Type: KeyHasProperties
Tags: typescript, javascript, react, programming
Published: 2023-11-22 11:20:50

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

```typescript
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. 

```typescript
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:

```typescript
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:

```typescript
const items = useAppSelector(selectAllItems('cart'));
```

Defining the selector is relatively simple:

```typescript
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:

```typescript
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.

```bash
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.


```bash
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. 


```bash
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:

```typescript
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:

```typescript
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.

```typescript
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`) 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 :)