Conditional Types in TypeScript

· 5 min read · Updated March 7, 2026 · advanced
conditional-types typescript types generics advanced

Conditional types are one of TypeScript’s most powerful type-system features. They allow you to create types that adapt based on other types, enabling truly generic and reusable type transformations. In this tutorial, you’ll learn how to harness conditional types to write more expressive and flexible TypeScript code.

Basic Conditional Type Syntax

A conditional type selects one of two types based on a type relationship:

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

type A = IsString<string>;  // true
type B = IsString<number>;   // false

The syntax reads like: “If T extends string, then the type is true, otherwise it’s false.”

This becomes powerful when combined with generics:

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

type A = Nullable<string>;   // string
type B = Nullable<null>;     // never
type C = Nullable<string | null>;  // string

The infer Keyword

The infer keyword lets you extract types from within other types—it’s like pattern matching for TypeScript:

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

function getUser() {
  return { id: 1, name: "Alice" };
}

type User = ReturnType<typeof getUser>;
// { id: number; name: string }

Here’s how infer works inside a conditional type:

// Extract the first element from an array
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;

type A = First<[string, number, boolean]>;  // string
type B = First<[]>;                          // never

Practical Conditional Types

Let’s build some useful conditional types from scratch:

Filter Out Nullish Types

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

type Clean = NonNullable<string | null | undefined | number>;
// string | number

Extract Function Parameters

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

function createUser(name: string, age: number, email: string) {
  return { name, age, email };
}

type Params = Parameters<typeof createUser>;
// [string, number, string]

Check if a Type Is Any

type IsAny<T> = 0 extends (1 & T) ? true : false;

type A = IsAny<any>;      // true
type B = IsAny<string>;   // false
type C = IsAny<never>;    // false

This works because any is the only type that can intersect with anything and produce 0.

Mapped Conditionals

Combine conditional types with mapped types for powerful transformations:

type MakeOptional<T> = {
  [K in keyof T]?: T[K];
};

type MakeRequired<T> = {
  [K in keyof T]-?: T[K];
};

interface User {
  id: number;
  name: string;
  email: string;
}

type OptionalUser = MakeOptional<User>;
type RequiredUser = MakeRequired<OptionalUser>;  // Back to original

Conditional Property Types

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

interface Config {
  api: {
    url: string;
    headers: Record<string, string>;
  };
  maxRetries: number;
}

type FrozenConfig = DeepReadonly<Config>;
// {
//   readonly api: {
//     readonly url: string;
//     readonly headers: Readonly<Record<string, string>>;
//   };
//   readonly maxRetries: number;
// }

Distributive Conditional Types

When the checked type is a union, conditional types become distributive:

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

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

This is why Exclude and Extract work:

// Exclude: remove T from U
type Exclude<T, U> = T extends U ? never : T;

// Extract: keep only T from U
type Extract<T, U> = T extends U ? T : never;

type A = Exclude<string | number, string>;  // number
type B = Extract<string | number, string>;   // string

Real-World Example: API Response Types

Here’s a practical example combining everything:

// Define API response shapes
interface ApiSuccess<T> {
  ok: true;
  data: T;
}

interface ApiError {
  ok: false;
  error: {
    code: number;
    message: string;
  };
}

type ApiResponse<T> = ApiSuccess<T> | ApiError;

// Type guard to narrow the response
function isSuccess<T>(response: ApiResponse<T>): response is ApiSuccess<T> {
  return response.ok === true;
}

// Usage
async function fetchUser(id: number): Promise<ApiResponse<{ id: number; name: string }>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    return { ok: true, data: await response.json() };
  } catch {
    return { ok: false, error: { code: 500, message: "Network error" } };
  }
}

// Narrowing the type
const result = await fetchUser(1);
if (isSuccess(result)) {
  console.log(result.data.name);  // TypeScript knows data exists
} else {
  console.error(result.error.message);  // TypeScript knows error exists
}

Built-in Conditional Types

TypeScript provides several built-in conditional types in the standard library:

TypeDescription
ReturnType<T>Extracts the return type of a function
Parameters<T>Extracts the parameter types as a tuple
InstanceType<T>Extracts the instance type of a class
Exclude<T, U>Excludes types from T that are assignable to U
Extract<T, U>Extracts types from T that are assignable to U
NonNullable<T>Removes null and undefined from T

Summary

Conditional types are essential for advanced TypeScript:

  • Basic syntax: T extends U ? X : Y chooses types based on relationships
  • infer: Extracts types from within other types
  • Distribution: Union types distribute across conditional types
  • Practical applications: Build utilities like Nullable, Optional, and ReturnType

Master conditional types, and you’ll unlock TypeScript’s full type-system potential. In the next tutorial, you’ll explore more advanced type patterns that build on these concepts.