---
title: 'Conditional Types'
source: 'https://academia.sh/en/courses/typescript/conditional-types'
course: TypeScript
language: en
updated: '2026-08-17T18:09:52+00:00'
license: 'CC BY-SA 4.0'
---

# Conditional Types

Type-level branching, the distribution rule over unions and closing off distribution, extracting a type piece with infer, and the limit of conditional return types.

The previous lesson used a notation of the form `T[A] extends number ? A : never` and
branched at the type level. A **conditional type** is the structure at the center of
the type system's computing ability: it picks one of two types depending on whether one
type is assignable to another.

Its notation is the type-level counterpart of the ternary operator:

$$
T \text{ extends } U \;?\; X : Y
$$

This lesson covers how the condition is evaluated, its special behavior over unions,
extracting a piece with `infer`, and the limits of the structure.

## Branching at the Type Level

```typescript
type IsNumber<T> = T extends number ? "number" : "not a number";

const a: IsNumber<number> = "number";
const b: IsNumber<string> = "not a number";
console.log(a, b);

const c: IsNumber<number> = "not a number";
```

```text
p1.ts(7,7): error TS2322: Type '"not a number"' is not assignable to type '"number"'.
```

`IsNumber<number>` resolves to `"number"`, `IsNumber<string>` to `"not a number"`. The
seventh line's diagnostic shows the resolution really happened: the compiler reports
the error by writing `"number"` in place of `IsNumber<number>`.

The word `extends` here does not mean inheritance but an **assignability** test: "can
`T` be put where `U` is expected?" The rules established in the Structural Type
Compatibility lesson apply here.

## Distribution

If a conditional type's type parameter is a union, the condition is evaluated
**separately** for each member and the results are combined into a union. This is
called **distribution**:

```typescript
type IsNumber<T> = T extends number ? "number" : "not a number";
type NonDistributive<T> = [T] extends [number] ? "number" : "not a number";

declare const distributed: IsNumber<number | string>;
declare const sealed: NonDistributive<number | string>;

const x: null = distributed;
const y: null = sealed;
```

```text
p2.ts(7,7): error TS2322: Type '"not a number" | "number"' is not assignable to type 'null'.
  Type '"not a number"' is not assignable to type 'null'.
p2.ts(8,7): error TS2322: Type '"not a number"' is not assignable to type 'null'.
```

The diagnostics spell out both types explicitly. `IsNumber<number | string>` has
become `"not a number" | "number"`; the condition was evaluated first for `number`,
then for `string`. `NonDistributive<number | string>` gave a single result:
`"not a number"`.

What closes off distribution is **wrapping the type parameter in a tuple**. In the
notation `[T] extends [U]`, what is being tested is no longer `T` but a single-element
tuple; it does not split into union members, and the condition is evaluated once.

Which of the two behaviors is wanted depends on intent. If every member of a union
needs to be transformed, distribution is needed. If the question being asked is "does
**all** of this type satisfy this condition?", it should be closed off — otherwise a
meaningless result like "partly a number" comes out for the `number | string` union.

Distribution only applies to **bare** type parameters: the left side of the condition
must be directly `T`. There is no distribution in notations like `T[]`, `[T]`, or
`{ value: T }`.

## Filtering: `Drop`

Distribution's most common use is eliminating a member from a union. `never` is
produced for the member to be filtered; `never` takes up no space in a union:

```typescript
type Drop<T, U> = T extends U ? never : T;

type Unit = "C" | "Pa" | "%";
type MeasurableUnit = Drop<Unit, "%">;

const units: MeasurableUnit[] = ["C", "Pa"];
console.log(units.join(","));
```

The output is `C,Pa`. If the line `const wrong: MeasurableUnit = "%";` is added at the
end of the file, after a blank line:

```text
p5.ts(9,7): error TS2322: Type '"%"' is not assignable to type 'MeasurableUnit'.
```

The steps of the computation are as follows: the condition is evaluated separately for
the three members; `"C"` and `"Pa"` are not assignable to `"%"` so they produce
themselves, and `"%"` produces `never`. The result is `"C" | "Pa" | never`, which is
`"C" | "Pa"`.

This transform's standard-library counterpart is `Exclude`, covered in the Utility
Types lesson.

## Extracting a Piece with `infer`

A type variable can be declared on the left side of the condition with the `infer`
keyword. During matching, the compiler finds the type corresponding to that variable:

```typescript
interface Measurement {
  id: string;
  value: number;
}

type ElementType<T> = T extends readonly (infer E)[] ? E : never;

declare const element: ElementType<Measurement[]>;
declare const stringElement: ElementType<readonly string[]>;
declare const nonArray: ElementType<number>;

const a: null = element;
const b: null = stringElement;
const c: null = nonArray;
```

```text
p3.ts(12,7): error TS2322: Type 'Measurement' is not assignable to type 'null'.
p3.ts(13,7): error TS2322: Type 'string' is not assignable to type 'null'.
```

`ElementType<Measurement[]>` has become `Measurement`, `ElementType<readonly
string[]>` has become `string`. The fourteenth line producing no diagnostic also
carries information: `ElementType<number>` is `never`, and `never` is assignable to
every type — including `null`.

The same technique is used to extract a payload type from a course-wide result type:

```typescript
interface Measurement {
  id: string;
  value: number;
}

type Result<D, E = string> =
  | { status: "success"; value: D }
  | { status: "error"; error: E };

type SuccessType<S> = S extends { status: "success"; value: infer D } ? D : never;
type ErrorType<S> = S extends { status: "error"; error: infer E } ? E : never;

declare const success: SuccessType<Result<Measurement>>;
declare const error: ErrorType<Result<Measurement, number>>;

const a: null = success;
const b: null = error;
```

```text
p4.ts(16,7): error TS2322: Type 'Measurement' is not assignable to type 'null'.
p4.ts(17,7): error TS2322: Type 'number' is not assignable to type 'null'.
```

Distribution is at work here too: `Result<Measurement>` is a two-member union;
computing `SuccessType` gives `Measurement` for the success member, `never` for the
error member, and the union reduces to `Measurement`.

The value of this pattern is being able to reach into a piece of a type **without
repeating its definition**. If `Result`'s structure changes, `SuccessType` changes with
it; no manually maintained link remains between the two.

## The Limit of Conditional Return Types

Conditional types can be used in function signatures, but they do not resolve inside
the body:

```typescript
type Formatted<T> = T extends number ? string : number;

function format<T extends number | boolean>(value: T): Formatted<T> {
  if (typeof value === "number") {
    return value.toFixed(1);
  }
  return 0;
}

console.log(format(21.4), format(true));
```

```text
p7.ts(5,5): error TS2322: Type 'string' is not assignable to type 'Formatted<T>'.
p7.ts(7,3): error TS2322: Type 'number' is not assignable to type 'Formatted<T>'.
```

Inside the body, `Formatted<T>` cannot be resolved because `T` is not yet known; the
compiler cannot infer that the `typeof value` test also narrows `T`. The diagnostic
comes on both branches.

This is a known limit of the type system. There are two fixes: using a single type
assertion in the body — a debt that makes the limit visible and accepts that the body
honors the promise made in the signature — or writing an overload instead of a
conditional type. The second is the structure built in the Function Types lesson, and
gives callers the same result.

General principle: conditional types are powerful for **computing a type**, not for
**driving an implementation**.

## Summary

- A conditional type picks one of two types based on an assignability test; `extends`
  here means assignability, not inheritance.
- If a bare type parameter is a union, the condition is evaluated separately for each
  member and the results are combined into a union; the notation `[T] extends [U]`
  closes off this distribution.
- Distribution together with `never` builds filters that eliminate a member from a
  union.
- `infer` declares a type variable on the left side of the condition and extracts the
  type corresponding to it during matching; it lets a piece of a type be reached
  without repeating its definition.
- Conditional return types do not resolve inside the function body; this case needs
  either a type assertion or an overload.

## Next Step

The key remapping lesson used a notation of the form `` `read${Capitalize<string &
A>}` `` and computed a field name. Combining and parsing strings at the type level is
a separate toolset. The next lesson covers template literal types and how they are
used together with `infer` to parse string patterns.
