Skip to content

Instantly share code, notes, and snippets.

@jbreckmckye
Created October 11, 2020 19:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jbreckmckye/ba20c58f005201988a1ed66f781f9b39 to your computer and use it in GitHub Desktop.
Save jbreckmckye/ba20c58f005201988a1ed66f781f9b39 to your computer and use it in GitHub Desktop.
Fun with distributions
// Fun with distributions
// You probably know that TypeScript has union types
type StringsAndNumbers = string | number;
// You also probably know about 'discriminated unions' or 'tagged unions',
// representing different kinds of structs
type Eventing =
| { kind: 'loading', data: void }
| { kind: 'error', data: Error }
| { kind: 'success', data: string }
// You might also know about conditional types
type IsNumber <T> = T extends number ? "yep" : "nope";
// But did you know that conditional types and unions play together in an interesting way?
// When you pass a 'naked' union to a conditional, it gets 'distributed'. That is, every branch
// of the condition is evaluated with each individual member of the union. Not the whole union.
type AreTheyNumbers = IsNumber<StringsAndNumbers>;
// ^ type of this is 'yep' | 'nope'
//
// Because we split the union, mapped to the condition, then re-unioned:
//
// StringsAndNumbers
// -> string ------> T extends number ? 'yep' : 'nope' ----> 'nope' ---|
// -> number ------> T extends number ? 'yep' : 'nope' ----> 'yep' ---|
// |---> 'nope' | 'yep'
// Compare that to not splitting the union
//
// StringsAndNumbers ---> ( string | number ) ? extends number ... -----> 'nope'
// OK. That's nice, but so what?
// Well, what if I told you, you could use conditionals to 'hack' distributions in your generic types?
// And use this to break up your unions
// Why would I want to do that? Consider our events again. What if we wanted a sendEvent function?
function sendEventNotVeryGood (name: string, data: any) {}
// This is bad because it has no real types.
// What I want is to
// a) pass the name of one of my events
// b) pass the corresponding data
// Let's use distributions to hack this
type NarrowUnion <Union extends { kind: string }, Kind> = Union extends any
? Union['kind'] extends Kind
? Union
: never
: never;
type EventPayload <Name, Event = NarrowUnion<Eventing, Name>> = Event extends { data: any }
? Event['data']
: never;
type ErrorEventPayload = EventPayload<'error'>
// ^ type is 'error'
// Or more generically
// Now let's apply that to our sendevent function
function sendEvent <K extends Eventing['kind']> (name: K, data: EventPayload<K>) {}
sendEvent('error', true) // <- complains true !== Error
sendEvent('success', 'hello'); // <-- is happy
sendEvent('loading', 5); // <-- complains number !== void
sendEvent('loading', undefined); // <-- is happy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment