If you write TypeScript day to day you probably use unions quite a bit. But have you ever found yourself writing a type and wanting to access the *members* of a union, be that one passed in as a type parameter, or defined elsewhere?

It’s not something that comes up a lot, but every so often it’s sorely missed. Like when you want two function parameters to follow the same ‘branch’ of a union.

Well, there’s a neat ‘trick’ involving conditional types that makes this easy.

## The problem

Let’s set up an example usecase to show what I mean. It’s a contrived example but should serve to make the concepts clear.

Imagine we have a type representing different events.

1 | type Events = |

(If you didn’t know, this pattern - a union of structs with a discriminating string key - is called ‘discriminated unions’. In the Haskell world we call them ‘tagged unions’)

And imagine we have a function that sends events

1 | function sendEvent (kind: string, data: any) { |

We want to type the `sendEvent`

function better so that

- the “kind” is a correct event kind
- the “data” is the
*right*data for the*kind*of event

Of course, in reality the simplest solution would just be to take an `event`

of type `Events`

, which would force the caller to pass a consistently formatted object. But let’s ignore that for the sake of our example.

## Our first attempt

Typing the `kind`

string is easy

1 | function sendEvent (kind: Events['kind'], data: any) {} |

What about `data`

? Well, I could type it the same way as `kind`

1 | function sendEvent (kind: Events['kind'], data: Events['data']) {} |

But now I have a problem. My `kind`

and my `data`

can mismatch:

1 | sendEvent('error', 'a string, not an Error'); // no type errors! |

This is no good! What we want is to ensure that both `kind`

and `data`

use the same ‘branch’ of the `Events`

union, i.e. refer to the same event.

## An idea

So here’s the trick. What I need to do is create a type mapping that uses a conditional type - i.e. `Foo extends Bar ? Baz : Bam`

Any type condition will do, even a check to `extends any`

:

1 | type NarrowByKind <Kind, Items extends { kind: string }> = Items extends any |

This lets us pick an item out of a collection of discriminated unions using `kind`

as the discriminant. If I `NarrowByKind('success')`

I get the struct `{ kind: 'success', data: string }`

. This is our first step.

The way this works is that `Items extends any ? ... : ...`

makes TS consider each member of `Items`

union “individually”. Then, when we check whether the `kind`

matches and return `Items`

, we’re only returning that “individual” item.

For all other conditions, we return `never`

. This makes `NarrowByKind`

return a union itself, of `Item | never | never`

, which gets normalised down to just `Item`

.

### Why does TypeScript do this?

The TS docs call this behaviour distributive conditional types, and it’s described thusly. You’ll be forgiving for struggling with the explanation though.

1 | Conditional types in which the checked type is a naked type parameter are called distributive conditional types. |

This is quite a dense description and involves some less-than-obvious terminology. A better way to understand might be to imagine if the behaviour *didn’t* work - imagine if a union wasn’t “split up” inside a conditional type.

Take this example. What should `Foo`

be?

1 | type SomePrimitives = boolean | string | number; |

If we compare whether the whole union `(boolean | string | number)`

extends `number`

, then the answer is false, meaning `Foo = 'nope`

. A comparison like this will almost always be false, because unions are heterogenous (that’s the point of them), so they’ll rarely reliably `extend`

*anything*.

However, what if we could ‘map’ the comparison over members of the union instead, like mapping a function over an array? Then we’d get a much more intuitive (and I’d say, useful) result `('nope' | 'nope' | 'yep')`

.

To do that, though, we need to ‘distribute’ the conditional type ‘over’ the union. And we only want to do that when the union is a ‘naked’ type parameter, i.e. not wrapped in a more complex construct like an `Array`

or a `Promise`

. Now the TS docs make a little more sense.

### Applying the science

Knowing the ‘trick’, we can now create a type helper for our events.

1 | type EventData <Kind, Event extends { data: any } = |

Let’s supply this to our `sendEvent`

function:

1 | function sendEvent <K extends Events['kind'], D extends EventData<K>> ( |

This leads to the following type checks, all demonstrating what we’d expect:

1 | // Check that success -> payload == string |

Easy! Now when someone asks you if you know `TypeScript distributive conditional types`

, you can answer with confidence: I do!