The guide to conditional types in TypeScript
Tags: typescript, webdev
Published: 2022-12-07 18:55:36

**Written by [Matteo Di Pirro](https://blog.logrocket.com/author/matteodipirro/)✏️**

Since version 2.8, TypeScript has introduced support for conditional types. They might be a niche feature, but, as we’ll see, they are a very useful addition that helps us write reusable code. 

In this article, we’re going to see what conditional types are and why we might have used them intensively, even without knowing it.

*   [What are conditional types?](#what-are-conditional-types)
    *   [Constraints on conditional types](#constraints-on-conditional-types)
*   [Type inference in conditional types](#type-inference-in-conditional-types)
*   [Distributive conditional types](#distributive-conditional-types)
*   [Inbuilt conditional types](#inbuilt-conditional-types)
    *   [`NonNullable<T>`](#nonnullable)
    *   [`Extract<T, U>`](#extract-and-exclude) and [`Exclude<T, U>`](#extract-and-exclude)
    *   [`Parameters<T>`](#parameters-and-returntype) and [`ReturnType<T>`](#parameters-and-returntype)
    *   [`ConstructorParameters<T>`](#constructorparameters-and-instancetype) and [`InstanceType<T>`](#constructorparameters-and-instancetype)
*   [Conclusion](#conclusion)

## What are conditional types?

Conditional types let us deterministically define type transformations depending on a condition. In brief, they are a ternary conditional operator applied at the type level rather than at the value level. 

Conditional types are defined as follows:

```typescript
type ConditionalType = SomeType extends OtherType ? TrueType : FalseType
```

In plain English, the definition above would be as follows:

> If a given type `SomeType` extends another given type `OtherType`, then `ConditionalType` is `TrueType`, otherwise it is `FalseType`.

As usual, `extends` here means that any value of type `SomeType` is also of type `OtherType`. 

Conditional types can be recursive; that is, one, or both, of the branches can themselves be a conditional type:

```typescript
type Recursive<T> = T extends string[] ? string : (T extends number[] ? number : never)

const a: Recursive<string[]> = "10" // works
const b: Recursive<string> = 10 // Error: Type 'number' is not assignable to type 'never'.
```

### Constraints on conditional types

One of the main advantages of conditional types is their ability to narrow down the possible actual types of a generic type. 

For instance, let’s assume we want to define `ExtractIdType<T>`, to extract, from a generic `T`, the type of a property named `id`. In this case, the actual generic type `T` must have a property named `id`. At first, we might come up with something like the following snippet of code:

```typescript
type ExtractIdType<T extends {id: string | number}> = T["id"]

interface NumericId {
	id: number
}

interface StringId {
	id: string
}

interface BooleanId {
	id: boolean
}

type NumericIdType = ExtractIdType<NumericId> // type NumericIdType = number
type StringIdType = ExtractIdType<StringId> // type StringIdType = string
type BooleanIdType = ExtractIdType<BooleanId> // won't work
```

Here, we made it explicit that `T` must have a property named `id`, with type either `string` or `number`. Then, we defined three interfaces: `NumericId`, `StringId`, and `BooleanId`. 

If we attempt to extract the type of the `id` property, TypeScript correctly returns `string` and `number` for `StringId` and `NumericId`, respectively. However, it fails for `BooleanId`: `Type 'BooleanId' does not satisfy the constraint '{ id: string | number; }'. Types of property 'id' are incompatible. Type 'boolean' is not assignable to type 'string | number'`. 

Still, how can we enhance our `ExtractIdType` to accept any type `T` and then resort to something like `never` if `T` did not define the required `id` property? We can do that using conditional types:

```typescript
type ExtractIdType<T> = T extends {id: string | number} ? T["id"] : never

interface NumericId {
	id: number
}

interface StringId {
	id: string
}

interface BooleanId {
	id: boolean
}

type NumericIdType = ExtractIdType<NumericId> // type NumericIdType = number
type StringIdType = ExtractIdType<StringId> // type StringIdType = string
type BooleanIdType = ExtractIdType<BooleanId> // type BooleanIdType = never
```

By simply moving the constraint in the conditional type, we were able to make the definition of `BooleanIdType` work. In this second version, TypeScript knows that if the first branch is true, then `T` will have a property named `id` with type `string | number`.

## Type inference in conditional types

It is so common to use conditional types to apply constraints and extract properties’ types that we can use a sugared syntax for that. For instance, we could rewrite our definition of `ExtractIdType` as follows:

```typescript
type ExtractIdType<T> = T extends {id: infer U} ? T["id"] : never

interface BooleanId {
	id: boolean
}

type BooleanIdType = ExtractIdType<BooleanId> // type BooleanIdType = boolean
```

In this case, we refined the `ExtractIdType` type. Instead of forcing the type of the id property to be of type string | number, we’ve introduced a new type `U` using the `infer` keyword. Hence, `BooleanIdType` won’t evaluate to `never` anymore. In fact, TypeScript will extract `boolean` as expected. 

`infer` provides us with a way to introduce a new generic type, instead of specifying how to retrieve the element type from the true branch. 

At the end of the post, we’ll see some useful inbuilt types relying on the `infer` keyword.

## Distributive conditional types

In TypeScript, conditional types are distributive over union types. In other words, when evaluated against a union type, the conditional type applies to all the members of the union. Let’s see an example:

```typescript
type ToStringArray<T> = T extends string ? T[] : never

type StringArray = ToStringArray<string | number>
```

In the example above, we simply defined a conditional type named `ToStringArray`, evaluating to `string[]` if and only if its generic parameter is `string`. Otherwise, it evaluates to `never`. 

Let’s now see how TypeScript evaluates `ToStringArray<string | number>` to define `StringArray`. First, `ToStringArray` distributes over the union:

```typescript
type StringArray = ToStringArray<string> | ToStringArray<number>
```

Then, we can replace `ToStringArray` with its definition:

```typescript
type StringArray = (string extends string ? string[] : never) | (number extends string ? number[] : never)
```

Evaluating the conditionals leaves us with the following definition:

```typescript
type StringArray = string[] | never
```

Since `never` is a subtype of any type, we can remove it from the union:

```typescript
type StringArray = string[]
```

Most of the times the distributive property of conditional types is desired. Nonetheless, to avoid it we can just enclose each side of the `extends` keyword with square brackets:

```typescript
type ToStringArray<T> = [T] extends [string] ? T[] : never
```

In this case, when evaluating `StringArray`, the definition of `ToStringArray` does not distribute anymore:

```typescript
type StringArray = ((string | number) extends string ? (string | number)[] : never)
```

Hence, since `string | number` does not extend, `string, StringArray` will become `never`. 

Lastly, the distributive property doesn’t hold if the union type is part of a larger expression (i.e., a function, object, or tuple), no matter if this larger expression appears before or after `extends`. Let’s see an example:

```typescript
type NonDistributiveFunction<T> = (() => T) extends (() => string | number) ? T : never
```

```typescript
type Fun1 = NonDistributiveFunction<string | boolean> // type Fun1 = never

type Fun2 = NonDistributiveFunction<string> // type Fun2 = string
```

## Inbuilt conditional types

This last section shows a few examples of conditional types defined by TypeScript’s standard library.

### `NonNullable<T>` <a name="nonnullable">

`NonNullable<T>` filters out the `null` and `undefined` values from a type `T`:

```typescript
type NonNullable<T> = T extends null | undefined ? never : T
type A = NonNullable<number> // number
type B = NonNullable<number | null> // number
type C = NonNullable<number | undefined> // number
type D = NonNullable<null | undefined> // never
```

### `Extract<T, U>` and `Exclude<T, U>` <a name="extract-and-exclude">

`Extract<T, U>` and are one the opposite of the other. The former filters the `T` type to keep all the types that are assignable to `U`. The latter, on the other hand, will keep the types that are not assignable to `U`:

```typescript
type Extract<T, U> = T extends U ? T : never
type Exclude<T, U> = T extends U ? never : T

type A = Extract<string | string[], any[]> // string[]
type B = Exclude<string | string[], any[]> // string

type C = Extract<number, boolean> // never
type D = Exclude<number, boolean> // number
```

In the example above when defining `A`, we asked TypeScript to filter out of `string | string[]` all the types that were not assignable to `any[]`. That would only be string, as `string[]` is perfectly assignable to `any[]`. On the contrary, when we defined `B`, we asked TypeScript to do just the opposite. As expected, the result is string, instead of `string[]`. 

The same argument holds for `C` and `D`. In the definition of `C`, number is not assignable to `boolean`. Hence, TypeScript infers `never` as a type. When it comes to defining `D`, instead, TypeScript keeps `number`.

### `Parameters<T>` and `ReturnType<T>` <a name="parameters-and-returntype">

`Parameters<T>` and `ReturnType<T>` let us extract all the parameter types and the return type of a function type, respectively:

```typescript
type Parameters<T> = T extends (...args: infer P) => any ? P : never
type ReturnType<T> = T extends (...args: any) => infer R ? R : any
type A = Parameters<(n: number, s: string) => void> // [n: number, s: string]
type B = ReturnType<(n: number, s: string) => void> // void

type C = Parameters<() => () => void> // []
type D = ReturnType<() => () => void> // () => void
type E = ReturnType<D> // void
```

`Parameters<T>` is a bit complex in its declaration. It basically produces a tuple type with all the parameter types (or `never` if `T` is not a function). 

In particular, `(...args: infer P) => any` indicates a function type where the actual type of all the parameters (`P`) gets inferred. Any function will be assignable to this, as there is no constraint on the type of the parameters, and the return type is `any`. 

Similarly, `ReturnType<T>` extracts the return type of a function. In this case, we use `any` to indicate that the parameters can be of any type. Then, we infer the return type `R`.

### `ConstructorParameters<T>` and `InstanceType<T>` <a name="constructorparameters-and-instancetype">

`ConstructorParameters<T>` and `InstanceType<T>` are the same things as `Parameters<T>` and `ReturnType<T>`, applied to constructor function types rather than to function types:

```typescript
type ConstructorParameters<T> = T extends new (...args: infer P) => any ? P : never
type InstanceType<T> = T extends new (...args: any[]) => infer R ? R : any

interface PointConstructor {
	new (x: number, y: number): Point
}

class Point {
	private x: number;

	private y: number;

	constructor(x: number, y: number) {
    		this.x = x;
	    	this.y = y
	}
}

type A = ConstructorParameters<PointConstructor> // [x: number, y: number]
type B = InstanceType<PointConstructor> // Point
```

## Conclusion

In this article, we explored conditional types in TypeScript. We started from the basic definition and how to use it to enforce constraints. We then saw how type inference works and explored the workings of the distributivity property of [union types](https://blog.logrocket.com/understanding-discriminated-union-intersection-types-typescript/). Lastly, we looked at some of the common utility conditional types defined by TypeScript: we analyzed their definitions and complemented them with a few examples. 

As we saw throughout this article, conditional types are a very advanced feature of the type system. However, we’ll likely end up using them almost on a daily basis because TypeScript’s standard library widely employs them. 

Hopefully, this post will help you write your own types to simplify your code and make it more readable and maintainable in the long run.

---
## [LogRocket](https://lp.logrocket.com/blg/signup): Full visibility into your web and mobile apps

[![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/signup)

[LogRocket](https://lp.logrocket.com/blg/signup) is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

[Try it for free.](https://lp.logrocket.com/blg/typescript-signup)