Skip to content

TypeScript Conditional Types — Complete Guide

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about TypeScript Conditional Types. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript conditional types select one type or another based on a condition — like an if/else for types — enabling powerful Metaprogramming patterns where types adapt based on input, extract inner types, and filter union members.

What You'll Learn

  • Conditional type syntax: T extends U ? X : Y
  • The infer keyword for type extraction
  • Distributive conditional types over unions
  • Recursive conditional types
  • Practical patterns: type filtering, unwrapping

Why It Matters

Conditional types are the foundation of TypeScript's utility types (ReturnType, Extract, NonNullable) and generic constraints. They let you build types that react to their inputs — making the type system programmable rather than declarative.

Real-World Use

Prisma's generated types use conditional types extensively — optional relations become User | null while required relations are User. The Parameters and ReturnType utilities (built with conditional types + infer) are used by Durga Antivirus Pro's API client to automatically derive types from endpoint handler functions.

Learning Path

flowchart LR
  A[Template Literal Types] --> B[Conditional Types]
  B --> C[Mapped Types]
  B --> D[You Are Here]
  C --> E[Utility Types]
  E --> F[Namespaces & Modules]

Basic Conditional Types

type IsNumber<T> = T extends number ? "yes" : "no";

type A = IsNumber<42>;      // "yes"
type B = IsNumber<string>;  // "no"
type C = IsNumber<number>;  // "yes"

Think of it as a ternary for types: "If T extends (matches) U, use type X, otherwise use type Y."

Nested Conditionals

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<string>;   // "string"
type B = TypeName<[1, 2]>;   // "object" (arrays are objects)
type C = TypeName<true>;     // "boolean"

The infer Keyword

infer lets you declare a type variable within the extends clause and capture a part of the matched type:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn1 = () => string;
type Fn2 = (x: number) => boolean;

type R1 = ReturnType<Fn1>; // string
type R2 = ReturnType<Fn2>; // boolean

Multiple infer Positions

type SwapPair<T> = T extends [infer A, infer B] ? [B, A] : T;

type Swapped = SwapPair<["hello", 42]>;
// [number, string] (positions swapped)

type Params<T> = T extends (...args: infer P) => any ? P : never;

type FnParams = Params<(name: string, age: number) => void>;
// [string, number]

Deep Infer

type UnwrapPromise<T> = T extends Promise<infer U>
  ? U extends Promise<infer V>
    ? V
    : U
  : T;

type A = UnwrapPromise<Promise<string>>;          // string
type B = UnwrapPromise<Promise<Promise<number>>>; // number
type C = UnwrapPromise<number>;                   // number

Distributive Conditional Types

When a conditional type is used on a bare generic type parameter, it distributes over unions:

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

type Result = ToArray<string | number>;
// = ToArray<string> | ToArray<number>
// = string[] | number[]

// Without distribution — wrap in [T]
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// = (string | number)[]

Filtering Union Members

type ExcludeNull<T> = T extends null | undefined ? never : T;

type Values = string | null | boolean | undefined;
type NonNullValues = ExcludeNull<Values>;
// string | boolean

This is how TypeScript's built-in Exclude<T, U> works:

type MyExclude<T, U> = T extends U ? never : T;
type Result = MyExclude<"a" | "b" | "c", "a">; // "b" | "c"

Extract

type MyExtract<T, U> = T extends U ? T : never;

type Result = MyExtract<"a" | "b" | "c", "a" | "b">;
// "a" | "b"

Practical Conditional Type Patterns

Nullable Detection

type IsNullable<T> = T extends null | undefined ? true : false;

type A = IsNullable<string>;          // false
type B = IsNullable<string | null>;  // true
type C = IsNullable<undefined>;      // true

Function Check

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

type A = IsFunction<() => void>;  // true
type B = IsFunction<string>;      // false

Array Element Extraction

type ElementType<T> = T extends (infer U)[] ? U : T;

type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<number>;   // number

JSON Types

type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

type JSONType<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends null ? "null" :
  T extends JSONValue[] ? "array" :
  T extends object ? "object" :
  "unknown";

Recursive Conditional Types

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K];
};

interface Config {
  name: string;
  nested: {
    port: number;
    deep: {
      flag: boolean;
    };
  };
}

type ReadonlyConfig = DeepReadonly<Config>;
// All properties and nested properties are readonly

Common Mistakes

1. Forgetting Distribution

type IsString<T> = T extends string ? true : false;
type Result = IsString<string | number>; // boolean (true | false), not false!

Wrap in [T] to prevent distribution if that's not what you want.

2. Not Handling the never Case

type Filter<T> = T extends string ? T : never;
type Result = Filter<string | number>; // string only

// But what about Filter<never>? Returns never.
// In distributed conditionals over empty union, the result is never.

3. Using infer in a Non-Conditional Position

// Error: infer is only valid in extends clause
// type Bad<T> = T extends infer U ? U : never; // OK
// type Bad2<T> = infer U; // Error

4. Recursive Conditionals Without a Base Case

Always ensure there's a base case that doesn't recurse:

type Flatten<T> = T extends (infer U)[]
  ? U extends (infer V)[] // Recursive
    ? Flatten<V>
    : U                   // Base case: non-array element
  : T;                    // Base case: not an array

5. Forgetting to Test with Union Types

Conditional types behave differently with unions (distribution). Always test with union inputs.

6. Over-Nesting Conditionals

// Hard to read
type Complex<T> =
  T extends A ? "a" :
  T extends B ? "b" :
  T extends C ? "c" :
  T extends D ? "d" : "other";

// Better: use helper types
type IsA<T> = T extends A ? true : false;
type IsB<T> = T extends B ? true : false;

Practice Questions

  1. What is the syntax for a conditional type? T extends U ? X : Y — if T extends (is assignable to) U, the type is X, otherwise Y.

  2. What does the infer keyword do? It declares a type variable inside the extends clause that captures a part of the matched type for use in the true branch.

  3. What is distributive conditional typing? When a conditional type on a bare generic distributes over each member of a union, applying the condition individually and unioning the results.

  4. How do you prevent distribution in a conditional type? Wrap both sides of extends in tuples: [T] extends [U] ? X : Y.

Challenge: Write a FunctionPropertyNames<T> conditional type that returns the keys of T whose values are functions. Use it to extract only the method names from an interface.

FAQ

Can conditional types be recursive?

Yes, TypeScript supports recursive conditional types, but there is a depth limit (usually 50 levels) to prevent infinite loops.

What is the difference between conditional types and mapped types?

Conditional types select between types (if/else). Mapped types iterate over keys to transform each property (loop).

Can I use `infer` with primitive types?

Yes: type StrLen<T> = T extends string ? infer L extends number ? L : never — but simpler approaches usually exist.

Do conditional types affect compilation speed?

Deeply nested or recursive conditionals can slow Type Checking. Keep them shallow unless necessary.

What is `never` in the context of conditional types?

In distributed conditionals, if the union is empty (never), the result is never — an empty union.

Mini Project: Type-Safe API Client

// src/api-client.ts

type APIMethods = "GET" | "POST" | "PUT" | "DELETE";
type Endpoints = {
  "/users": { GET: User[]; POST: UserCreate };
  "/users/:id": { GET: User; PUT: UserUpdate; DELETE: void };
  "/scans": { GET: Scan[]; POST: ScanRequest };
};

type ExtractResponse<T, M extends APIMethods> =
  T extends { [K in M]: infer R } ? R : never;

type RequestBody<T, M extends APIMethods> =
  M extends "POST" | "PUT"
    ? T extends { [K in M]: infer R } ? R : never
    : undefined;

class APIClient {
  async request<E extends keyof Endpoints, M extends APIMethods>(
    endpoint: E,
    method: M,
    body?: RequestBody<Endpoints[E], M>
  ): Promise<ExtractResponse<Endpoints[E], M>> {
    const response = await fetch(endpoint as string, {
      method,
      body: body ? JSON.stringify(body) : undefined,
    });
    return response.json();
  }
}

interface User { id: string; name: string; }
interface Scan { id: string; threats: string[]; }

const client = new APIClient();
const users = await client.request("/users", "GET");
// type: User[]

const scan = await client.request("/scans", "POST", { /* type-checked */ });

What's Next

Now explore mapped types for transforming object types:

Lesson Description
{{< ref "/programming-languages/typescript/12-template-literal-types" >}} Review template literal types
{{< ref "/programming-languages/typescript/14-mapped-types" >}} Partial, Required, Readonly, Pick, Record
{{< ref "/programming-languages/typescript/15-utility-types" >}} Omit, Extract, Exclude, NonNullable, ReturnType

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro