TypeScriptConditional Types

Conditional Types

Conditional types let you choose between two types based on a condition evaluated at the type level. The syntax mirrors JavaScript's ternary operator — but it runs entirely at compile time, making your types adaptive and expressive.

Basic Syntax

The fundamental form is:

T extends U ? X : Y

Read it as: "If T is assignable to U, resolve to X; otherwise resolve to Y."

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

type A = IsString<string>;   // true
type B = IsString<number>;   // false
type C = IsString<"hello">;  // true  (string literal extends string)
type D = IsString<42>;       // false
Note
The extends here is a type constraint check, not class inheritance. It asks: "Is T assignable to U?"
How Conditional Types Are Evaluated

TypeScript resolves conditional types by substituting the actual type argument for T and checking whether it satisfies the constraint U. If T is a concrete type at the time of evaluation, the result is immediate.

If T is still a generic type parameter (not yet resolved), TypeScript defers the evaluation until it has more information — typically when the generic is instantiated at a call site.

TS
// Immediate evaluation — T is known
type Result1 = string extends string | number ? "yes" : "no"; // "yes"
type Result2 = boolean extends string ? "yes" : "no";          // "no"

// Deferred evaluation — T is still generic inside the function
function process<T>(value: T): T extends string ? "str" : "other" {
  // TypeScript defers resolution here, so a cast is needed
  return (typeof value === "string" ? "str" : "other") as any;
}

const r1 = process("hello"); // "str"
const r2 = process(42);      // "other"
Distributive Conditional Types

When you apply a conditional type to a naked type parameter (a plain generic, not wrapped in anything), TypeScript automatically distributes the condition over each member of a union. This is one of the most powerful — and sometimes surprising — behaviours in the type system.

TS
type ToArray<T> = T extends any ? T[] : never;

// With a union, distribution kicks in automatically:
type Result = ToArray<string | number>;
// Step 1: ToArray<string> | ToArray<number>
// Step 2: string[] | number[]

type A = ToArray<string>;          // string[]
type B = ToArray<string | number>; // string[] | number[]
type C = ToArray<boolean>;         // false[] | true[]  (boolean = false | true)
Tip
Distribution happens only when the checked type is a bare (naked) generic parameter. Wrapping it stops distribution — see the next section.
Non-Distributive Conditional Types

Sometimes you want to treat the union as a whole rather than distributing over its members. Wrap T (and U) in a single-element tuple [T] on both sides to opt out of distribution.

TS
// Distributive — splits the union, returns boolean (true | false)
type IsString_D<T> = T extends string ? true : false;
type D = IsString_D<string | number>; // boolean

// Non-distributive — treats union as a single unit
type IsString_ND<T> = [T] extends [string] ? true : false;
type ND = IsString_ND<string | number>; // false

// Another example: check if T is exactly string (not a union containing string)
type IsExactlyString<T> = [T] extends [string] ? ([string] extends [T] ? true : false) : false;

type X1 = IsExactlyString<string>;         // true
type X2 = IsExactlyString<string | number>; // false
type X3 = IsExactlyString<"hello">;        // false (literal, not exactly string)
Warning
Forgetting to wrap with tuples when you need non-distributive behaviour is a common source of subtle bugs in generic utility types.
Practical Examples — Type Predicates

Here are several reusable conditional types you will encounter or write in real codebases:

TS
// Is T exactly never?
type IsNever<T> = [T] extends [never] ? true : false;

type A = IsNever<never>;   // true
type B = IsNever<string>;  // false

// Is T a mutable array?
type IsArray<T> = T extends any[] ? true : false;

type C = IsArray<number[]>;          // true
type D = IsArray<readonly string[]>; // false

// Extend to include readonly arrays
type IsArrayLike<T> = T extends readonly any[] ? true : false;
type E = IsArrayLike<readonly string[]>; // true

// Is T a function?
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;

type F = IsFunction<() => void>;             // true
type G = IsFunction<(x: string) => number>;  // true
type H = IsFunction<string>;                 // false

// Is T a Promise?
type IsPromise<T> = T extends Promise<any> ? true : false;

type I = IsPromise<Promise<string>>; // true
type J = IsPromise<string>;          // false
Extracting and Excluding with Conditional Types

TypeScript's built-in Extract and Exclude utility types are both implemented using conditional types under the hood. Knowing this helps you understand how to build your own type-level filters.

TS
// Built-in implementations (simplified)
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;

type Primitives = string | number | boolean | object | symbol;

// Keep only string and number
type StrOrNum = Extract<Primitives, string | number>; // string | number

// Remove object
type NoObjects = Exclude<Primitives, object>; // string | number | boolean | symbol

// Custom: keep only nullable members of a union
type OnlyNullable<T> = T extends null | undefined ? T : never;
type N = OnlyNullable<string | null | undefined | number>; // null | undefined

// Custom: remove functions from a type
type NonFunction<T> = T extends (...args: any[]) => any ? never : T;
type M = NonFunction<string | (() => void) | number>; // string | number
Nested Conditional Types

Conditional types can be chained to build more complex type logic — just like nested ternaries in JavaScript, but at the type level.

TS
type TypeName<T> =
  T extends string    ? "string"    :
  T extends number    ? "number"    :
  T extends boolean   ? "boolean"   :
  T extends undefined ? "undefined" :
  T extends null      ? "null"      :
  T extends Function  ? "function"  :
  "object";

type T1 = TypeName<string>;     // "string"
type T2 = TypeName<42>;         // "number"
type T3 = TypeName<true>;       // "boolean"
type T4 = TypeName<() => void>; // "function"
type T5 = TypeName<{ a: 1 }>;   // "object"
type T6 = TypeName<null>;       // "null"
Combining with infer (Preview)

The real power of conditional types emerges when combined with the infer keyword, which lets you capture a piece of a type during the extends check and use it in the true branch. The infer keyword is covered in depth in its own page — here is a quick preview:

TS
// Extract the return type of any function
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = (x: number) => string;
type R = MyReturnType<Fn>; // string

// Unwrap a Promise one level
type Unwrap<T> = T extends Promise<infer V> ? V : T;

type P = Unwrap<Promise<number>>; // number
type Q = Unwrap<string>;          // string (not a Promise, returns T as-is)

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

type H1 = Head<[string, number, boolean]>; // string
type H2 = Head<[]>;                        // never
Tip
The infer keyword only works inside a conditional type's extends clause. You cannot use infer outside of a T extends ... ? ... : ... expression.
Common Patterns at a Glance

Pattern

Code

Use case

Type guard flag

T extends X ? true : false

Boolean indicator for a type

Type filter

T extends X ? T : never

Keep only matching members

Type remover

T extends X ? never : T

Remove matching members

Non-distributive check

[T] extends [X] ? … : …

Treat union as a whole

Type extractor

T extends Wrap<infer U> ? U : never

Unwrap a wrapper type

Real-World Example — DeepReadonly

Combining conditional types with recursion lets you build utilities that operate at any nesting depth. Here is a DeepReadonly type that marks every nested property and array element as readonly:

TS
type DeepReadonly<T> =
  T extends (infer U)[]
    ? ReadonlyArray<DeepReadonly<U>>
    : T extends object
      ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
      : T;

interface Config {
  server: {
    host: string;
    ports: number[];
  };
  debug: boolean;
}

type FrozenConfig = DeepReadonly<Config>;
// {
//   readonly server: {
//     readonly host: string;
//     readonly ports: ReadonlyArray<number>;
//   };
//   readonly debug: boolean;
// }

const cfg: FrozenConfig = {
  server: { host: "localhost", ports: [3000, 8080] },
  debug: true,
};

// cfg.debug = false;           // Error: cannot assign to 'debug' (readonly)
// cfg.server.ports.push(9000); // Error: push does not exist on ReadonlyArray
Success
You now understand conditional types: the ternary syntax, distributive vs non-distributive behaviour, practical filtering patterns, and how infer plugs in. These are the building blocks for almost every advanced TypeScript utility type.