---
title: "TypeScript conditional types and how infer works"
description: "TypeScript conditional types pick a type based on assignability. Covers the T extends X ? Y : Z syntax, the infer keyword, and distributive conditional types."
canonical: https://omiid.me/notebook/23/typescript-conditional-types-and-how-infer-works
author: Omid Sayfun
published: 2026-05-04
updated: 2026-08-09
---

# TypeScript conditional types and how infer works

A conditional type picks one of two types based on whether one type is assignable to another. It is TypeScript's ternary operator, applied at the type level.

The syntax is:

```typescript
type Result = A extends B ? C : D;
```

The word `extends` is the confusing part. This is not class inheritance. It means **assignability**: can a value of type `A` be used where a `B` is expected? If yes, resolve to `C`. If no, resolve to `D`.

```typescript
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
type C = IsString<"hello">; // "yes" - string literals are assignable to string
```

This is purely a compile-time check. Nothing happens at runtime.

The official docs for the feature live in the TypeScript handbook, under [conditional types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html). The two parts that cause the most confusion in real code are distribution over unions and pattern matching with `infer`.

## The infer keyword pulls a type out of another type

`infer` is what makes conditional types genuinely useful. It lets you extract a type from inside another type while you're pattern-matching it.

```typescript
type UnwrapPromise<T> = T extends Promise<infer Inner> ? Inner : T;

type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number (not a Promise, so returns T)
```

You're saying: "if `T` matches `Promise<something>`, capture that something into `Inner`." TypeScript figures out what `Inner` must be.

This is how `ReturnType<T>` is implemented in the standard library:

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

A few more patterns that come up in real code:

```typescript
// Extract the first argument type of a function
type FirstArg<T extends (...args: any) => any> = T extends (
	first: infer F,
	...rest: any
) => any
	? F
	: never;

type F = FirstArg<(id: string, options: { limit: number }) => void>;
// F = string

// Unwrap an array element type
type ElementOf<T> = T extends Array<infer E> ? E : T;

type E = ElementOf<string[]>; // string
type F = ElementOf<number>; // number (passthrough)
```

One thing worth noting: you don't always need `infer`. If the type is accessible through indexing, that's simpler:

```typescript
// No need for infer here
type Value = Record<string, number>[string]; // number
```

Save `infer` for when you need to pattern-match a shape that TypeScript can't reach through direct indexing.

## Distributive conditional types split the union before the check runs

This is where most conditional type bugs come from.

When the type being checked (`T`) is a **naked type parameter** (not wrapped in a tuple, object, or anything else), TypeScript automatically distributes the condition across each member of a union:

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

type A = ToArray<string | number>;
// TypeScript expands this to:
// ToArray<string> | ToArray<number>
// = string[] | number[]
```

That's often exactly what you want. But sometimes it isn't:

```typescript
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string | number>;
// = "yes" | "no"  (distributed: each member checked separately)
```

If you want to check the **whole union** against `string`, wrap both sides in square brackets to opt out of distribution:

```typescript
type IsString<T> = [T] extends [string] ? "yes" : "no";

type A = IsString<string | number>;
// = "no"  (the union as a whole is not assignable to string)
```

The `[T] extends [string]` form checks assignability once on the tuple, so distribution doesn't kick in.

## Three places conditional types earn their keep

### Extract an event payload from a discriminated union

Say you have a union of app events:

```typescript
type AppEvent =
	| { type: "user.created"; payload: { id: string; email: string } }
	| { type: "order.placed"; payload: { orderId: string; amount: number } }
	| { type: "item.removed"; payload: { itemId: string } };
```

You can extract the payload for a specific event type without conditional types, because `Extract` and index access is enough:

```typescript
type PayloadFor<T extends AppEvent["type"]> = Extract<
	AppEvent,
	{ type: T }
>["payload"];

type UserCreatedPayload = PayloadFor<"user.created">;
// { id: string; email: string }
```

But if your event structure is more complex or the shape varies, `infer` is worth using:

```typescript
type ExtractPayload<T, K extends string> = T extends {
	type: K;
	payload: infer P;
}
	? P
	: never;

type P = ExtractPayload<AppEvent, "order.placed">;
// { orderId: string; amount: number }
```

### Give a function a conditional return type

When the return shape depends on a literal argument, a conditional return type ties the two together in one signature:

```typescript
type FetchResult<T extends "raw" | "parsed"> = T extends "raw"
	? Buffer
	: { data: unknown; status: number };

function fetch<T extends "raw" | "parsed">(
	url: string,
	format: T,
): Promise<FetchResult<T>>;
```

Callers get the right return type automatically based on what they pass for `format`. No overloads needed.

### Strip null and undefined the way NonNullable does

Distribution does the real work in this utility type, so the body is a single line:

```typescript
type StripNullish<T> = T extends null | undefined ? never : T;

type A = StripNullish<string | null | undefined>; // string
```

The distributive behavior is what makes this work. TypeScript applies the condition to each union member, strips the nullish ones, and merges the rest. This is exactly how the built-in `NonNullable<T>` works.

## Reach for overloads or discriminated unions first

Conditional types add cognitive overhead. Before writing one, check if the problem fits a simpler tool.

**Function overloads** are often cleaner when you need different return types based on argument types:

```typescript
function process(input: string): string[];
function process(input: number): number;
function process(input: string | number): string[] | number {
	// ...
}
```

**Discriminated unions** work better when you're narrowing based on a shared property in application code:

```typescript
type Result = { ok: true; value: string } | { ok: false; error: Error };
```

Use conditional types when:

- You're writing a utility type that transforms or extracts from a type parameter
- You need to pattern-match a shape you can't reach through direct indexing
- You're building library-level or shared utility types

If you're in application code and squinting at the type signature, it's probably overengineered. Discriminated unions or overloads will be easier for whoever reads it next, including you.

---

For a longer worked example, I wrote [a similar deep dive into building a type-safe groupBy function in TypeScript](https://omiid.me/notebook/45/a-try-at-type-safe-groupby-function-in-typescript) that puts some of these patterns to use.
