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