Skip to main content

TypeScript Conditional Types

A conditional type is a type that depends on another type. It works like an if-else statement, but for types instead of values.


Understanding Conditional Types in TypeScript

Conditional types let you build types that change depending on the type you give them. They make advanced type transformations and type-level programming possible.

Key concepts:

  • Type-level logic: run a condition on a type
  • Type inference: pull types apart with infer
  • Composition: combine with other TypeScript features
  • Utility types: build reusable type helpers

Common use cases:

  • Type-safe function overloading
  • Transforming API response types
  • Complex type validation
  • Reusable type utilities
  • Advanced type inference

Basic Conditional Type Syntax

A conditional type has the form T extends U ? X : Y. Read it as: if type T extends (is assignable to) type U, use type X, otherwise use type Y.

Example

IsString<T> becomes true for string and for the literal "hello", because a literal extends its base type. For number it becomes false. Variables can use the resulting type too.

Prompt
Show the basic T extends U ? X : Y conditional type syntax. Declare IsString<T> as T extends string ? true : false, then aliases Result1, Result2 and Result3 for string, number and the literal "hello" with comments giving true, false, true, and two variables a: IsString<string> and b: IsString<number> with comments noting their types 'true' and 'false'. Types only, nothing is printed.
Output: TypeScript file and captured output

Conditional Types with Unions

Conditional types are especially useful with union types. When the checked type is a union, the condition is applied to each member separately. This is called a distributive conditional type.

Distributive Conditional Types

ToArray<string | number> is split into ToArray<string> | ToArray<number>, which gives string[] | number[]. The same distribution lets ExtractString keep only the string members of a union by mapping the others to never.

Prompt
Show how a conditional type distributes over the members of a union. Declare ToArray<T> as T extends any ? T[] : never and StringOrNumberArray = ToArray<string | number> with comments explaining it becomes string[] | number[], then ExtractString<T> that keeps only string members and StringsOnly applied to string | number | boolean | "hello" with a comment giving string | "hello". Types only, nothing is printed.
Output: TypeScript file and captured output

Type Inference with infer

The infer keyword declares a type variable inside the condition of a conditional type. If the condition matches, the variable holds the captured type and you can use it in the true branch.

Extracting Types from Complex Structures

ReturnType captures the return type R of a function type. ElementType captures the element type U of an array type. With typeof greet you can feed a real function's type into these helpers.

Prompt
Show the infer keyword capturing a return type and an array element type. Declare ReturnType<T> as T extends (...args: any[]) => infer R ? R : never, two functions greet() returning "Hello, world!" and getNumber() returning 42, and aliases GreetReturnType/NumberReturnType using typeof with comments giving string and number. Then declare ElementType<T> that infers U from (infer U)[] and aliases for number[] and string[]. Types only, nothing is printed; make the file a module so the local ReturnType does not clash with the global one.
Output: TypeScript file and captured output

Built-in Conditional Types

TypeScript's standard library already includes several conditional types, so you do not need to write the common ones yourself.

Standard Library Utilities

Extract keeps the members of a union that match, Exclude removes them, NonNullable drops null and undefined, and Parameters and ReturnType take a function type apart.

Prompt
Show the built-in conditional utility types from the standard library. Declare OnlyStrings = Extract<string | number | boolean, string>, NoStrings = Exclude<string | number | boolean, string>, NotNull = NonNullable<string | null | undefined>, Params = Parameters<(a: string, b: number) => void> and Return = ReturnType<() => string>, each with a comment explaining the helper and the resulting type. Types only, nothing is printed.
Output: TypeScript file and captured output

Advanced Patterns and Techniques

Conditional types can call themselves, be chained like an if-else ladder, and describe the return type of a generic function.

Recursive Conditional Types

A conditional type can refer to itself. UnwrapPromise keeps unwrapping until the type is no longer a Promise, so even a promise inside a promise resolves to the plain inner type.

Prompt
Show a recursive conditional type that deeply unwraps Promise types. Declare UnwrapPromise<T> as T extends Promise<infer U> ? UnwrapPromise<U> : T, then aliases A, B and C for Promise<string>, Promise<Promise<number>> and boolean with comments giving string, number and boolean. Types only, nothing is printed.
Output: TypeScript file and captured output

Type-Level If-Else Chains

Several conditions can be chained, one after the other. TypeName checks a type against string, number, boolean, undefined and Function in turn, and falls back to "object".

Prompt
Show a chain of conditional types that acts like a type-level if-else ladder. Declare TypeName<T> that maps string, number, boolean, undefined and Function to the string literals "string", "number", "boolean", "undefined" and "function", falling back to "object". Add aliases T0 to T4 for string, 42, true, () => void and Date[] with comments showing the results. Types only, nothing is printed.
Output: TypeScript file and captured output

Conditional Return Types

Conditional types shine in generic utilities and type-safe libraries. Here a generic function's return type depends on the type of its argument. Inside the function, as any is needed because TypeScript cannot check the body against a conditional return type.

Prompt
Show a generic function whose return type is a conditional type based on its argument. Write processValue<T>(value: T) returning string for strings, number for numbers, boolean for booleans and never otherwise; upper-case strings, double numbers, negate booleans (each with an as any assertion) and throw Unsupported type for anything else. Call it with "hello", 10 and true, storing stringResult, numberResult and boolResult with comments noting "HELLO", 20 and false. Nothing is printed.
Output: TypeScript file and captured output

Best Practices

Do:

  • Use conditional types for complex type transformations
  • Combine them with infer to extract parts of a type
  • Create reusable type utilities
  • Document complex conditional types
  • Test the edge cases of your type definitions

Don't:

  • Reach for complex conditional types when a simple type is enough
  • Build deeply nested conditional types that are hard to read
  • Forget that very complex types slow down the compiler
  • Use conditional types for runtime logic — they exist only at compile time

Performance considerations:

  • Deeply nested conditional types increase compile times
  • Store intermediate results in type aliases
  • Keep TypeScript's recursion depth limits in mind