Hedronite Lesson · Polyglot-Dev / TypeScript · Thu 2026-09-24

Conditional types and infer

Type-level ternary. Infer a piece. Filter the union by hand.

Lesson Class: Duha (TypeScript Primary track)
Focus: T extends U ? X : Y · distributive unions · infer · Extract vs hand-rolled filter
Code Blocks: clean blocks, explanation in prose
Done-criteria: conditional with infer + Extract vs hand-rolled filter
Grounding: Cherny Ch.6 Conditional Types / infer (pp.195–200) · Handbook Conditional Types + Extract/Exclude
The ternary
T extends U ? X : Y decides at the type level.
The pull
infer names a matched piece inside the true branch.
The filter
Exclude/Extract are stock; ByKind shows a custom predicate.
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.

Related