Skip to content

Conditional Types

A conditional type looks like this:

T extends U ? X : Y

It reads like:

“If T is assignable to U, then return X, otherwise return Y.”

This is TypeScript’s version of an if/else at the type level.


Basic Example

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

Usage:

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

This is the foundation for more advanced patterns.


Conditional Types with Unions

Conditional types distribute over unions.

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

type Result = IsString<string | number>
// true | false

This is called distributive conditional types, and it’s incredibly powerful.


Filtering Unions

You can use conditional types to filter unions.

Example: Keep only strings

type OnlyStrings<T> = T extends string ? T : never

Usage:

type Mixed = string | number | boolean

type StringsOnly = OnlyStrings<Mixed>
// string

Example: Remove strings

type WithoutStrings<T> = T extends string ? never : T

type NoStrings = WithoutStrings<Mixed>
// number | boolean

This is how Exclude<T, U> is implemented.


Conditional Types with Functions

You can extract information from function types.

Example: Extract return type

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

Usage:

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

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

This is exactly how ReturnType<T> works.


Inferring Types with infer

infer lets you capture a type inside a conditional type.

It’s like saying:

“If this pattern matches, extract the type inside and call it R.”


Example: Extract function argument type

type FirstArg<T> =
  T extends (arg: infer A, ...rest: any[]) => any
    ? A
    : never

Usage:

type A = FirstArg<(x: number, y: string) => void>
// number

Example: Extract array element type

type Element<T> = T extends (infer U)[] ? U : never

Usage:

type E = Element<string[]> // string

This is how Awaited<T> and many array utilities work.


Example: Extract Promise result

type UnwrapPromise<T> =
  T extends Promise<infer R> ? R : T

Usage:

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

This is the foundation of async type utilities.


Real‑World Example: Deep Readonly

Using conditional types + mapped types + infer:

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

Usage:

type User = {
  id: number
  profile: {
    name: string
    email: string
  }
}

type Frozen = DeepReadonly<User>

Result:

  • all nested properties become readonly
  • works recursively
  • no runtime cost

This is how advanced libraries implement deep transformations.


Real‑World Example: Extracting Event Payloads

Imagine an event map:

type Events = {
  login: { userId: string }
  logout: { userId: string }
  error: { message: string }
}

We can extract payload types:

type EventPayload<E extends keyof Events> = Events[E]

But we can also infer event names from payloads:

type EventNameFromPayload<P> =
  { [K in keyof Events]: Events[K] extends P ? K : never }[keyof Events]

Usage:

type Name = EventNameFromPayload<{ userId: string }>
// "login" | "logout"

Conditional types let you build dynamic, type‑safe event systems.


Putting It All Together

Here’s a powerful example combining everything:

type AsyncResult<T> =
  T extends Promise<infer R>
    ? { type: "promise"; value: R }
    : T extends (...args: any[]) => infer F
      ? { type: "function"; value: F }
      : { type: "other"; value: T }

Usage:

type A = AsyncResult<Promise<number>>
// { type: "promise"; value: number }

type B = AsyncResult<() => string>
// { type: "function"; value: string }

type C = AsyncResult<boolean>
// { type: "other"; value: boolean }

This is the kind of expressive type logic that makes TypeScript so powerful.


Summary

In this lesson, you learned how conditional types enable type‑level logic:

1. T extends U ? X : Y

  • type‑level branching
  • filtering unions
  • transforming types

2. infer

  • extract return types
  • extract argument types
  • unwrap arrays and promises
  • build advanced utilities

Conditional types + infer are the backbone of TypeScript’s advanced type system.

They let you write types that compute, transform, and adapt—just like real code.