Conditional types and infer
Types that branch.
A conditional type is an if/else at the type level β T extends U ? X : Y β and infer lets you capture a piece of a matched type, which is how ReturnType, ElementType, and Flatten are all built.
The ternary at the type level
T extends U ? X : Y reads exactly like a runtime ternary, but it runs in the type checker. If T is assignable to U, the type resolves to X; otherwise to Y. Because T is usually a generic parameter, the branch is decided per instantiation β IsString<'hi'> resolves to one branch, IsString<number> to the other.
Capturing types with infer
Inside the extends clause you can introduce a fresh type variable with infer. It says "match this shape, and bind whatever sits in this position to a name". T extends Array<infer E> ? E : never matches any array and hands you its element type E. infer only ever appears in the true-branch's extends clause β that is where the pattern match happens.
Distribution over unions
When the checked type is a naked type parameter (T on its own, not wrapped), a conditional type distributes over a union: Foo<A | B> becomes Foo<A> | Foo<B>. This is usually what you want, but it surprises people. To switch it off, wrap both sides in a one-tuple β [T] extends [U] ? β¦ β so the union is checked as a whole.
Building the standard library by hand
ReturnType, Parameters, Awaited, and NonNullable are all just conditional types with infer. Writing them yourself is the fastest way to internalise both features β once you can write ReturnType and Flatten, the rest of the type-level toolbox falls out.
Try it 5 examples
IsString<T> β a type-level predicate
TSintroThe runtime values {"a":true,"b":false,"c":true} are only assignable because the compiler resolved each conditional to that exact literal β proof that IsString<'hello'> and IsString<string> both took the true branch while IsString<number> took the false branch.