Conditional Types in TypeScript: extends, infer, and Distribution
- typescript
- javascript
- advanced-types
- frontend
- backend
Conditional types are where TypeScript stops feeling like a type checker and starts feeling like a programming language that runs at compile time. They let a type make a decision — "if the input type is this, produce that type, otherwise produce this other one" — and combined with the infer keyword, they let you pull pieces out of other types. This is the machinery behind a lot of the utility types you already use, and while application code needs it less often than generics or unions, knowing it is what lets you solve the hard typing problems without giving up and reaching for any.
This is part of the TypeScript type system series. It assumes you are comfortable with generics, since conditional types are generics with branching.
The basic form
A conditional type uses the same extends ? : shape as a ternary, but at the type level:
type IsString<T> = T extends string ? true : false
type A = IsString<"hello"> // true
type B = IsString<number> // false
Read T extends string ? true : false as "if T is assignable to string, the type is true, otherwise it is false." The extends here is the assignability test — the same subset relationship from the type system guide, asking whether T's set of values fits inside string's. Conditional types become genuinely useful when the branches return meaningful types rather than booleans, and when they are combined with generics so the decision depends on a type parameter.
A practical example — flatten a type that might or might not be an array:
type ElementType<T> = T extends any[] ? T[number] : T
type X = ElementType<string[]> // string
type Y = ElementType<number> // number (already not an array)
If T is an array, the type resolves to its element type; if it is not, it passes through unchanged. That is a small decision, but it is the kind of normalization that removes special-casing from the code that consumes the type.
infer extracts types from within types
The real power arrives with infer, which lets you capture a piece of a type into a new type variable inside the condition. You place infer X where you want to grab something, and if the pattern matches, X is bound to whatever was there.
type ElementType<T> = T extends (infer E)[] ? E : T
This reads as "if T matches the pattern 'array of some element type', capture that element type as E and return it." infer E is a placeholder that says "I don't know what the element type is, but bind it to E so I can use it in the true branch." It is pattern matching for types.
This is exactly how the built-in ReturnType works. It matches the shape of a function and infers the return type out of it:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
function getUser() {
return { id: "1", name: "Aman" }
}
type User = ReturnType<typeof getUser> // { id: string; name: string }
The condition asks "does T look like a function returning some type R?" and if so, infer R captures that return type. Parameters, Awaited, and many other utilities are the same trick applied to different parts of a type — the argument list, the value a Promise resolves to, and so on. Once you can read infer, those utility types stop being magic and become short, legible programs.
Distribution over unions
There is one behavior of conditional types that surprises everyone the first time, and it is important to understand because it is usually what you want but occasionally is not. When the type you test is a union, the conditional distributes over each member of the union independently, then joins the results back into a union.
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number>
// distributes to: ToArray<string> | ToArray<number>
// which is: string[] | number[]
Rather than treating string | number as one thing, TypeScript applies the conditional to string and to number separately and unions the outcomes. This distributive behavior is what makes conditional types compose cleanly with unions, and it is why utilities like Exclude can filter a union member by member. When you specifically do not want distribution — you want to test the whole union as a unit — you wrap both sides in a tuple to switch it off: [T] extends [any] ? .... Knowing the switch exists saves you from a confusing afternoon the day distribution does something you did not intend.
Use it sparingly and deliberately
A word of judgment, because conditional types are the part of TypeScript most prone to over-use. Application code rarely needs them; the vast majority of good TypeScript is generics, unions, and narrowing, with conditional types appearing mainly inside reusable library-style utilities. When you find yourself writing deeply nested conditional types to model ordinary business data, that is usually a sign the underlying types should be simpler, not that you need more type-level cleverness. Clever types are hard to read, slow to compile, and produce error messages that make people cry.
The right use is targeted: a single conditional that removes a real special case, a small utility that keeps two types in sync, an infer that extracts a type you would otherwise have to duplicate by hand. Used that way, conditional types are a precise tool for the handful of problems that genuinely need type-level computation. They pair naturally with mapped types, which handle iterating over a type's keys while conditionals handle the branching — together they are the two halves of programming in the type system, to be reached for when a real problem calls for them and left on the shelf the rest of the time.
Key takeaways
- A conditional type is a type-level ternary: T extends U ? X : Y, where extends is the assignability (subset) test.
- The infer keyword pattern-matches inside a type, capturing a piece — like a function's return type — into a new type variable.
- Built-in utilities like ReturnType and Parameters are just conditional types with infer applied to different parts of a type.
- Conditional types distribute over unions by default, applying to each member and rejoining; wrap sides in tuples to switch distribution off.
- Use them sparingly — mostly inside reusable utilities; deeply nested conditionals for ordinary business data usually signal the types should be simpler.
Frequently asked questions
What is a conditional type in TypeScript?
A type that chooses between two types based on a condition, written T extends U ? X : Y. It reads as "if T is assignable to U, the result is X, otherwise Y." Combined with generics, it lets a type depend on a type parameter — the basis of many utility types.
What does infer do?
infer captures part of a type into a new variable inside a conditional. For example, T extends (infer E)[] ? E : T binds E to an array's element type if T is an array. It is pattern matching for types, and it is how ReturnType extracts a function's return type.
What is distribution in conditional types?
When the tested type is a union, the conditional applies to each member separately and unions the results — so ToArray<string | number> becomes string[] | number[]. This is usually desirable; to test the whole union as one unit, wrap both sides in tuples like [T] extends [U].
Should I use conditional types in application code?
Rarely. Most good TypeScript is generics, unions, and narrowing. Conditional types belong mostly in reusable, library-style utilities. Deeply nested conditionals to model ordinary business data usually mean the underlying types should be simplified, not that more type-level cleverness is needed.
Further reading
Explore more on

About the author
I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.