Conditional Types

10 questions found

What is the basic syntax of a conditional type, and how does it resemble a ternary expression?

Beginner
A conditional type has the form `T extends U ? X : Y` — evaluated at the type level, it resolves to X if T is assignable to U, and Y otherwise, mirroring the syntax and logic of a runtime ternary expression but operating purely on types.
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>;      // false
Real-world example Building a reusable type-level check to conditionally select a return type based on an input type parameter.

Common follow-ups: Can conditional types be nested to express multiple branches, like a chain of else-if?

Generics

How would you write a simple conditional type to extract the element type of an array?

Beginner
Combine a conditional type with the 'infer' keyword to capture and name the array's element type only when T actually matches the array shape.
type ElementType<T> = T extends (infer U)[] ? U : never;
type A = ElementType<string[]>; // string
type B = ElementType<number>;   // never
Real-world example Deriving the item type from a generic list type without manually specifying it twice.

Common follow-ups: What does the resulting type become if T isn't an array at all?

Never Unknown & Void Types

What does the 'infer' keyword do inside a conditional type?

Intermediate
infer introduces a new type variable within the 'extends' clause of a conditional type, letting TypeScript capture and name a piece of the matched type structure for use in the true branch — essentially pattern-matching on types.
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
type A = ReturnTypeOf<() => string>; // string
Real-world example Extracting a function's return type automatically, which is exactly how TypeScript's built-in ReturnType<T> utility type works.

Common follow-ups: Can you use infer more than once within the same conditional type?

Mapped Types

How do distributive conditional types behave when T is a union type?

Intermediate
When the checked type T is a naked type parameter (not wrapped in something like [T]) and you pass a union as T, the conditional type automatically distributes over each member of the union individually, then unions the results back together.
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>; // string[] | number[], NOT (string | number)[]
Real-world example Understanding why a conditional type applied to a union produces a union of individually-transformed results, which can be surprising at first.

Common follow-ups: How would you prevent distribution and force the union to be treated as a single type?

Union & Intersection Types

How do you prevent a conditional type from distributing over a union?

Intermediate
Wrap both sides of 'extends' in a single-element tuple (like [T] extends [U]), which makes T no longer a 'naked' type parameter — this disables the automatic distribution behavior and treats the union as one combined type instead.
type IsUnion<T, U = T> = T extends U ? ([U] extends [T] ? false : true) : never;
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type A = ToArrayNonDist<string | number>; // (string | number)[]
Real-world example Writing a utility type that needs to test a union AS A WHOLE, rather than transforming each member separately.

Common follow-ups: Why does this trick with a wrapping tuple actually work to suppress distribution?

Union & Intersection Types

How would you write a conditional type that recursively flattens a nested array type of arbitrary depth?

Advanced
Use a conditional type combined with infer and recursion: if T matches an array of arrays, recurse into the inner array type; otherwise, return T as the base case — the compiler will keep re-evaluating the type alias against itself until it bottoms out.
type DeepFlatten<T> = T extends (infer U)[] ? DeepFlatten<U> : T;
type A = DeepFlatten<number[][][]>; // number
Real-world example Deriving the final, fully unwrapped element type from a deeply nested array structure without manually specifying the depth.

Common follow-ups: Does TypeScript place any limit on how deep recursive conditional types can go?

Generics

How does TypeScript's built-in Exclude<T, U> utility type work internally, using conditional types?

Advanced
Exclude<T, U> is implemented as a distributive conditional type: `type Exclude<T, U> = T extends U ? never : T;` — because it distributes over a union T, each member that matches U is individually replaced with never (which effectively disappears from the resulting union), leaving only the members that don't match.
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
Real-world example Understanding how many of TypeScript's built-in utility types (Exclude, Extract, NonNullable) are just thin wrappers over conditional types.

Common follow-ups: How would you implement Extract<T, U>, the logical opposite of Exclude?

Union & Intersection Types

How would you implement a type-level 'if-else chain' to classify a type into one of several categories?

Advanced
Chain multiple conditional types together, where the false branch of one conditional type is itself another conditional type — mirroring a JavaScript if/else-if/else chain, evaluated entirely at the type level.
type TypeName<T> =
  T extends string ? 'string' :
  T extends number ? 'number' :
  T extends boolean ? 'boolean' :
  T extends undefined ? 'undefined' :
  T extends Function ? 'function' :
  'object';
type A = TypeName<42>; // 'number'
Real-world example Building a type-level classifier used internally by advanced utility types or a runtime type-checking helper's return type.

Common follow-ups: Is there a practical limit to how many conditions you can chain before the compiler slows down significantly?

Never Unknown & Void Types

How does a conditional type distinguish between 'any' and other types, given any is assignable to (and from) everything?

Advanced
Because 'any' is compatible with virtually any type check, a naive conditional type applied to 'any' tends to resolve to a union of BOTH branches (the true and false results) rather than picking one — since the compiler can't determine which branch is correct when the input could be anything.
type Test<T> = T extends string ? 'yes' : 'no';
type A = Test<any>; // 'yes' | 'no' -- both branches, not just one
Real-world example Debugging a confusing conditional type result that unexpectedly includes both branches, tracing it back to an 'any' leaking into the type parameter.

Common follow-ups: How would you write a conditional type that specifically detects whether a type IS 'any'?

Never Unknown & Void Types

How do you write a type that recursively converts every property of a nested object type to be optional, using conditional types alongside mapped types?

Advanced
Combine a mapped type (to iterate keys) with a conditional type (to check if each property's value is itself an object, and recurse if so) — this lets you deep-partial an arbitrarily nested object type, unlike the built-in Partial<T> which only affects the top level.
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

interface Config { server: { host: string; port: number }; }
type PartialConfig = DeepPartial<Config>;
// { server?: { host?: string; port?: number } }
Real-world example Typing a configuration-merging function that accepts partial overrides at any nesting level, not just the top level.

Common follow-ups: Why does the built-in Partial<T> utility type NOT recurse into nested objects by default?

Mapped Types