Created
October 11, 2020 19:01
-
-
Save jbreckmckye/ba20c58f005201988a1ed66f781f9b39 to your computer and use it in GitHub Desktop.
Fun with distributions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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