Things I didn't know about TypeScript

Image by Masha Menshikova, CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0), via Wikimedia Commons

I picked the image because I thought it'd be a fun representation of generics.

Damn! There's a lot you don't know. You think you do, but then it hits you in the face. I've been reading up a bit on TypeScript again and going through the utility types. I thought it'd be fun to reimplement them. Easy enough, right? Turns out I had a lot of misconceptions when it comes to TypeScript.

For example, Exclude is defined as the following.

type Exclude<T, U> = T extends U ? never : T

// for example Exclude<'a' | 'b' | 'c',  'c'> results in 'a' | 'b'

I didn't get this definition. How do we end up with a smaller union by just using the extends keyword? I thought of T and U as singular, so if T doesn't "extend" U we'd just end up in the never case. Turns out I missed a bit about using conditional types, or more specifically distributive conditional types.

When conditionally extending in a union, the extends statement distributes the condition over all the types in the union. If we do a simple example by attempting to extend string, and then doing something in both cases then we get the following.

type X<A extends string | number> = A extends string ? `string ${A}` : `number ${A}`
type Y =  X<'a' | 'b' | 1 | 2>

// type Y = "string a" | "string b" | "number 1" | "number 2"

What I would have thought would happen is that we're not able to do extends string with our type and end up in the else statement with all the values. This is possible however by simply wrapping the type in square brackets like the following.

type X<A extends string | number> = [A] extends [string] ? `string ${A}` : `number ${A}`
type Y =  X<'a' | 'b' | 1 | 2>

// type Y = "number a" | "number b" | "number 1" | "number 2"

The more you know! Think of the possibilities!

Oh yeah, and NonNullable is this (in TypeScript 4.8.0).

type NonNullable<T> = T & {}

I'm just not gonna think about it.