Advanced0 questionsFull Guide

TypeScript Conditional Types — Complete Interview Guide

Deep-dive into TypeScript conditional types — the T extends U ? X : Y syntax, distributive behaviour over unions, the infer keyword for type extraction, and how to build powerful type utilities like ReturnType, Awaited, and NonNullable from first principles.

The Mental Model

Conditional types are TypeScript's type-level ternary operator — the type equivalent of the value-level expression condition ? thenValue : elseValue. At the type level, T extends U ? X : Y means: 'if type T is assignable to type U, resolve to type X, otherwise resolve to type Y.' The infer keyword extends this further, letting you declare a type variable inside the extends clause to capture and name a sub-type that TypeScript infers during the assignability check — like pattern matching for types. Together they form a type-level programming system powerful enough to implement ReturnType, Awaited, Parameters, and dozens of real utility types.

The Explanation

Basic Conditional Type Syntax

// T extends U ? X : Y
// "If T is assignable to U, the type is X. Otherwise the type is Y."

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<"hello">; // true — "hello" extends string

// Practical: NonNullable (removes null and undefined from T)
type NonNullable<T> = T extends null | undefined ? never : T;

type D = NonNullable<string | null>;        // string
type E = NonNullable<number | undefined>;   // number
type F = NonNullable<null | undefined>;     // never

Distributive Conditional Types

When the type parameter is a naked type variable (not wrapped in anything), conditional types distribute over union members automatically — the condition is applied to each union member separately and the results are combined:

// T is naked (not wrapped) — distributes over unions
type ToArray<T> = T extends any ? T[] : never;

type StringOrNumberArrays = ToArray<string | number>;
// Distributes: ToArray<string> | ToArray<number>
// = string[] | number[]
// (NOT (string | number)[])

// Exclude is a distributive conditional type
type Exclude<T, U> = T extends U ? never : T;
type Status = "active" | "inactive" | "deleted";
type ActiveOrInactive = Exclude<Status, "deleted">;
// Distributes:
//   "active" extends "deleted" ? never : "active"   → "active"
//   "inactive" extends "deleted" ? never : "inactive" → "inactive"
//   "deleted" extends "deleted" ? never : "deleted"  → never
// Result: "active" | "inactive"

// To PREVENT distribution — wrap T in a tuple
type NoDistribute<T> = [T] extends [any] ? T[] : never;
type Wrapped = NoDistribute<string | number>;
// [string | number] extends [any] = true → (string | number)[]
// (NOT string[] | number[])

The infer Keyword

infer declares a type variable to be inferred by TypeScript during the extends check. It's the mechanism that makes ReturnType, Parameters, and Awaited possible:

// ReturnType — infer R is bound to the return type of the function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser(): User { return { id: 1, name: "Alice" }; }
type UserType = ReturnType<typeof getUser>; // User

// Parameters — infer P is bound to the parameter tuple
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type GetUserParams = Parameters<typeof getUser>; // []

// Unwrap a Promise — infer the resolved type
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
// Recursively unwraps: Promise<Promise<string>> → string

type Resolved = Awaited<Promise<User[]>>; // User[]

// Extract array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type ItemType = ArrayElement<string[]>; // string

// Extract the first element of a tuple
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type FirstArg = Head<[string, number, boolean]>; // string

Chained Conditional Types

// Multiple conditions — like if/else if/else chains
type TypeName<T> =
  T extends string  ? "string"  :
  T extends number  ? "number"  :
  T extends boolean ? "boolean" :
  T extends symbol  ? "symbol"  :
  T extends object  ? "object"  :
  "other";

type A = TypeName<string>;      // "string"
type B = TypeName<42>;          // "number"
type C = TypeName<() => void>;  // "object"

// Nested conditional for deep type extraction
type UnwrapNested<T> =
  T extends Promise<infer U> ? UnwrapNested<U> :
  T extends Array<infer U>   ? UnwrapNested<U> :
  T;

Conditional Types in Mapped Types

// Filter properties by value type using conditional type in mapped key position
type PickByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};

interface Config {
  host: string;
  port: number;
  debug: boolean;
  maxRetries: number;
}

type StringConfig = PickByValue<Config, string>;
// { host: string; }

type NumberConfig = PickByValue<Config, number>;
// { port: number; maxRetries: number; }

// Make only certain value-typed fields optional
type OptionalStrings<T> = {
  [K in keyof T]: T[K] extends string ? T[K] | undefined : T[K];
};

Common Misconceptions

⚠️

Many developers think T extends U in a conditional type means T must inherit from U like in class inheritance — it means T must be assignable to U. 'hello' extends string is true because string literals are assignable to string, with no inheritance relationship.

⚠️

Many developers think conditional types always distribute over unions — distribution only happens when the type parameter is a naked type variable at the top level. Wrapping in a tuple ([T] extends [U]) prevents distribution, which is sometimes what you want.

⚠️

Many developers think infer can be used outside of an extends clause — infer is only valid inside the true branch's extends clause. It's not a general type inference tool; it's specifically a pattern-matching capture variable.

⚠️

Many developers think conditional types are evaluated eagerly — when T is still a generic type parameter (not yet resolved), TypeScript defers conditional type evaluation. This can cause 'Type is not assignable' errors that seem counterintuitive until you understand deferred evaluation.

⚠️

Many developers think never in a union disappears — never is the identity element for unions. string | never === string. This is why distributive conditional types that return never for some members effectively filter those members out of the union.

⚠️

Many developers think conditional types are only for advanced library authors — ReturnType<typeof fn>, Awaited<T>, and Parameters<typeof fn> are daily-use conditional types. Understanding infer makes reading type errors and library types significantly easier.

Where You'll See This in Real Code

React component prop extraction: ComponentProps<typeof Button> uses conditional types to extract the props interface from a component — invaluable when a library doesn't export its prop types directly.

Async function return type unwrapping: Awaited<ReturnType<typeof fetchUser>> gives you the resolved User type without having to manually inspect the promise chain — a clean pattern for typing API responses.

Type-safe event systems: EventMap extends Record<string, unknown> combined with conditional type inference lets an event emitter's on() method infer the callback parameter type from the event name string.

GraphQL code generation: tools generate conditional types to model nullable fields, optional arguments, and union return types accurately from GraphQL schemas — conditional types handle the nullable wrapping cleanly.

tRPC and type-safe RPCs: tRPC's router inference uses deep conditional type extraction to flow types from server procedure definitions to client call sites with zero manual type declarations.

ORM query builder types: Prisma and TypeORM use conditional types to model relations, select sub-objects, and optional includes — the returned type changes based on which fields you select in the query options.

Interview Cheat Sheet

  • Syntax: T extends U ? X : Y — if T is assignable to U, resolve to X, else Y
  • Distributive: naked type parameter T distributes over union members automatically
  • Prevent distribution: wrap in tuple — [T] extends [U] ? X : Y — evaluates T as a whole
  • infer: captures a sub-type inside extends clause — T extends Promise<infer U> ? U : never
  • never in conditional type results filters union members (never | A = A)
  • Chained: T extends A ? X : T extends B ? Y : Z — type-level if/else if/else
  • Combine with mapped type as clause to filter keys: [K as T[K] extends V ? K : never]
  • ReturnType<T>: T extends (...args: any[]) => infer R ? R : never
  • Awaited<T>: T extends Promise<infer U> ? Awaited<U> : T — recursive unwrap
  • Deferred evaluation: conditional types over unresolved generics are deferred — may produce unexpected errors
💡

How to Answer in an Interview

  • 1.Start by demystifying the syntax: 'T extends U ? X : Y is the type-level ternary. extends here means assignable to, not inherits from. "hello" extends string is true because string literals are assignable to string.'
  • 2.The infer keyword explanation is what separates intermediate from senior: 'infer lets me declare a new type variable that TypeScript fills in during the assignability check — like a capture group in a regex. ReturnType works by checking if T matches a function shape and capturing the return type as R.'
  • 3.Distribution is the nuanced behaviour to mention: 'When T is a naked generic, conditional types distribute over union members. Exclude<'a' | 'b' | 'c', 'c'> applies the condition to each member separately. Wrapping in a tuple prevents this — [T] extends [U] evaluates T as a complete union.'
  • 4.Ground it in everyday use: 'I use conditional types most often through ReturnType, Awaited, and Parameters. Understanding the underlying infer mechanism means I can read library type definitions and error messages more clearly, which saves significant debugging time.'

Practice Questions

No questions tagged to this topic yet.

Related Topics

TypeScript Union & Intersection Types — Complete Interview Guide
Intermediate·6–10 Qs
TypeScript Mapped Types — Complete Interview Guide
Advanced·6–10 Qs
TypeScript Generics — Complete Interview Guide
Intermediate·8–12 Qs
TypeScript Utility Types — Complete Interview Guide
Intermediate·6–10 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz