VOOZH about

URL: https://blog.logrocket.com/guide-conditional-types-typescript/

⇱ The guide to conditional types in TypeScript - LogRocket Blog


2022-11-29
1828
#typescript
Matteo Di Pirro
142816
👁 Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

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.

👁 Conditional types TypeScript

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

🚀 Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

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:

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:

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:

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:

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:

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:

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.


Over 200k developers use LogRocket to create better digital experiences

👁 Image
Learn more →

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

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

Then, we can replace ToStringArray with its definition:

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

Evaluating the conditionals leaves us with the following definition:

type StringArray = string[] | never

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

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:

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

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

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:

type NonDistributiveFunction<T> = (() => T) extends (() => string | number) ? T : never
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>

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

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>

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:

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>

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

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>

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:

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. 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 understands everything users do in your web and mobile apps.

👁 LogRocket Dashboard Free Trial Banner

LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks, and with plugins to log additional context from Redux, Vuex, and @ngrx/store.

With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.

Modernize how you understand your web and mobile apps — start monitoring for free.

👁 Image
👁 Image
👁 Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

I benchmarked Claude Code and OpenCode on a heavy refactor: The reality of agentic CLI workflows

Claude Code vs. OpenCode in a real Next.js refactor: benchmark results, mistakes, prompts, and when to use each CLI agent.

👁 Image
Chizaram Ken
May 28, 2026 ⋅ 11 min read

The 5 Claude skills for React I can’t live without

Every time you explain your team’s coding standards to Claude, you are doing work that should be reusable. The same […]

👁 Image
Chizaram Ken
May 27, 2026 ⋅ 10 min read

Stop trying to one-shot: How to prompt Claude better

Learn how to move beyond one-shot prompting in Claude with structured workflows for AI-assisted coding, debugging, PR reviews, documentation, testing, and automation.

👁 Image
Peter Aideloje
May 26, 2026 ⋅ 18 min read

How to build advanced forms in Next.js using a rule engine

Learn how to build advanced Next.js forms with rule engines, client-side previews, Server Actions, and server-validated form logic.

👁 Image
Ikeh Akinyemi
May 21, 2026 ⋅ 18 min read
View all posts

Would you be interested in joining LogRocket's developer community?

Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now