Conditional types and infer
Type-level ternary. Infer a piece. Filter the union by hand.
<!-- hal:authoritative:yaml -->
*Ask at the type level: does T extend U? Branch on the answer. Pull a piece out with infer. Filter a union by hand before you lean on Extract.*
§I - Frame
Duha Primary TypeScript session 9. Topics #16: **conditional types and infer**. Generics and object utilities shipped 2026-09-18 (Topics #9). Supply-chain / lockfile shipped yesterday (Topics #15). Bun-centric rows 5-8 and syllabus #11-13 stay deferred. This fire stays on one spine: the type-level ternary, infer inside it, and one hand-rolled union filter you can compare to Extract / Exclude.
Done-criteria: Can write a conditional type with infer and explain Extract vs a hand-rolled filter.
Primary cite: Cherny Programming TypeScript Chapter 6 (Conditional Types, infer, built-ins) on disk at . Secondary: live Handbook Conditional Types and Utility Types for Extract / Exclude.
Home: .
§II - The type-level ternary
Cherny frames a conditional type as: declare a type that depends on U and V; if U is a subtype of V, resolve to A, otherwise to B. The surface syntax matches a value ternary:
type IsString<T> = T extends string ? true : false
type A = IsString<string> // true
type B = IsString<number> // false
T extends string is the condition. The true branch and false branch are types, not values. Nest when you need more than two outcomes. Use the form anywhere a type is legal: aliases, parameters, defaults.
The power shows up with generics. A fixed check like Dog extends Animal you can answer by eye. A check against an open T encodes a decision the call site has not fixed yet. Handbook's createLabel rewrite is the same idea: one conditional return type replaces a pile of overloads.
§III - Distribution over unions
When the checked type is a bare type parameter, TypeScript distributes the conditional across each member of a union. Cherny's Without (the same shape as built-in Exclude) is the clearest proof:
type Without<T, U> = T extends U ? never : T
type Kept = Without<boolean | number | string, boolean>
// number | string
Walk-through: the union splits into three checks; boolean extends boolean becomes never; the other two keep their members; never drops out of the union. Without distribution you would get one check of the whole union against U and a wrong answer.
That distribution is why filter helpers work. Keep it in mind when a conditional "eats" a union in a way you did not expect: you can wrap both sides in a tuple ([T] extends [U] ? …) to turn distribution off, per the Handbook.
§IV - infer inside the true branch
infer declares a fresh type variable inside the condition so the true branch can name a piece of structure without asking the caller to pass it. Cherny's array element example:
// indexed access (no infer)
type ElementType<T> = T extends unknown[] ? T[number] : T
// same idea with infer
type ElementType2<T> = T extends (infer U)[] ? U : T
type N = ElementType2<number[]> // number
If you put U in the outer angle brackets instead, the caller must supply it and the helper stops being useful. infer is for extraction the type checker performs for you.
A practical second form: unwrap a Promise result type.
type AwaitedSimple<T> =
T extends Promise<infer Inner> ? Inner : T
type P = AwaitedSimple<Promise<string>> // string
type Q = AwaitedSimple<number> // number
Handbook's GetReturnType follows the same pattern on function signatures: match (...args) => infer R, resolve to R. One conditional with infer replaces reading the return type by hand.
§V - Hand-rolled filter vs Extract / Exclude
Built-ins are thin wrappers on the same idea:
// lib shape (conceptual)
type Exclude<T, U> = T extends U ? never : T // Cherny Without
type Extract<T, U> = T extends U ? T : never // keep assignable members
Exclude removes members assignable to U. Extract keeps them. Prefer the built-in when the rule is exactly that. Hand-roll when you need a tighter predicate, for example filtering by a discriminant field:
type Event =
| { kind: "click"; x: number; y: number }
| { kind: "key"; key: string }
| { kind: "idle" }
// hand-rolled: keep only object members whose kind is assignable to K
type ByKind<E, K extends string> =
E extends { kind: K } ? E : never
type Clicks = ByKind<Event, "click">
// { kind: "click"; x: number; y: number }
type BuiltIn = Extract<Event, { kind: "click" }>
// same result for this shape
ByKind is Extract specialized to a kind check. When the filter is "members assignable to this object type," Extract is enough and clearer. When the filter needs a named parameter (K) or a custom true-branch reshape, write the conditional yourself. That is the comparison the done-criteria asks for: same distributive engine, different predicate.
§VI - Closing
Conditional types are type-level ternaries (T extends U ? X : Y). They distribute over unions when T is a naked parameter, which is how union filters work. infer names a piece of matched structure in the true branch so helpers like element-of-array or Promise-inner can compute without a second type argument. Exclude / Extract are the stock filters; a hand-rolled ByKind shows when a custom predicate earns its keep. Bun rows 5-8 and #11-13 stay deferred. Next Primary TypeScript topic follows the syllabus after #16 once a teammate seats it.
Done-criteria: conditional with infer + explain Extract vs a hand-rolled filter.