Table of Contents
- Introduction
- What Are Conditional Types in TypeScript?
- Basic Syntax of Conditional Types
- Understanding
extendsin Conditional Types - The
inferKeyword in Conditional Types - Distributive Conditional Types
- Practical Examples
- Example 1: Using
extendsin Conditional Types - Example 2: Using
inferfor Type Inference - Example 3: Distributive Conditional Types
- Example 1: Using
- Best Practices for Using Conditional Types
- Conclusion
Introduction
Conditional types are one of the most powerful features in TypeScript, enabling developers to define types that depend on certain conditions. They allow you to conditionally assign a type based on another type, offering greater flexibility in type definitions.
Conditional types are expressed with the following syntax:
T extends U ? X : Y
Where:
Tis the type being checked.Uis the type being tested against.Xis the result if the conditionT extends Uistrue.Yis the result if the conditionT extends Uisfalse.
Conditional types open up possibilities for dynamic and context-sensitive type assignments, enabling a more powerful and expressive type system.
This article will dive into the key components of conditional types, such as extends, infer, and distributive conditional types. We’ll explore each concept with detailed examples and show you how to use them effectively.
What Are Conditional Types in TypeScript?
Conditional types allow you to create types that depend on a condition. They are particularly useful when you need to define types that vary based on the structure or properties of other types.
For example, let’s say you want to create a type that is different depending on whether the provided type is a string or a number. With conditional types, you can express this as:
type MyType<T> = T extends string ? "String type" : "Other type";
Here, MyType will resolve to "String type" if the type passed in T is string, and "Other type" otherwise.
Basic Syntax of Conditional Types
The basic syntax for conditional types in TypeScript is:
T extends U ? X : Y
T: The type that is checked against the condition.U: The type that is being tested.X: The resulting type if the condition is true (T extends U).Y: The resulting type if the condition is false (T extends U).
Example:
type IsString<T> = T extends string ? "Yes" : "No";
type Test1 = IsString<string>; // "Yes"
type Test2 = IsString<number>; // "No"
In this example:
IsString<string>resolves to"Yes"becausestringextendsstring.IsString<number>resolves to"No"becausenumberdoes not extendstring.
Understanding extends in Conditional Types
The extends keyword plays a pivotal role in conditional types. It is used to check whether a given type extends (or is compatible with) another type. If the type extends, the first part of the conditional type (X) is chosen; otherwise, the second part (Y) is used.
Example: Using extends in Conditional Types
type IsArray<T> = T extends any[] ? "Array" : "Not an Array";
type Result1 = IsArray<number[]>; // "Array"
type Result2 = IsArray<string>; // "Not an Array"
Here:
IsArray<number[]>resolves to"Array"becausenumber[]extendsany[].IsArray<string>resolves to"Not an Array"becausestringdoes not extendany[].
This type of conditional logic is extremely helpful when you need to differentiate between various types and provide tailored behavior accordingly.
The infer Keyword in Conditional Types
The infer keyword is another powerful feature that TypeScript provides for working with conditional types. It allows you to infer a type within a conditional expression.
Example: Using infer for Type Inference
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
type Func1 = ReturnTypeOf<() => string>; // string
type Func2 = ReturnTypeOf<(a: number) => void>; // void
type Func3 = ReturnTypeOf<() => number[]>; // number[]
Here:
ReturnTypeOfchecks if the typeTextends a function type. If it does, it infers the return type (R) of the function. Otherwise, it returnsnever.- In the case of
Func1, the return type of the function is inferred asstring. - In
Func2, the return type is inferred asvoidbecause the function doesn’t return anything. Func3returns anumber[]since the function returns an array of numbers.
Why infer Is Powerful
The infer keyword enables TypeScript to dynamically capture types within a conditional block, making it extremely useful for type transformations or complex type inference.
Distributive Conditional Types
Distributive conditional types are a special case in TypeScript where conditional types distribute over union types. This means that if you pass a union type to a conditional type, TypeScript applies the condition to each member of the union.
Example: Distributive Conditional Types
type IsString<T> = T extends string ? "Yes" : "No";
type Test = IsString<string | number>; // "Yes" | "No"
Here:
IsString<string | number>is distributed over the union, meaning the type is evaluated for bothstringandnumber. This results in"Yes" | "No".
This behavior is highly useful when you need to create types that work flexibly with multiple possible types, as it allows TypeScript to apply conditions to each type individually.
Practical Examples
Example 1: Using extends in Conditional Types
type IsBoolean<T> = T extends boolean ? "Yes" : "No";
type Test1 = IsBoolean<boolean>; // "Yes"
type Test2 = IsBoolean<number>; // "No"
In this example:
IsBoolean<boolean>resolves to"Yes"becausebooleanextendsboolean.IsBoolean<number>resolves to"No"becausenumberdoes not extendboolean.
Example 2: Using infer for Type Inference
type FunctionReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Result1 = FunctionReturnType<() => number>; // number
type Result2 = FunctionReturnType<(x: string) => boolean>; // boolean
Here:
FunctionReturnTypeusesinferto capture the return type of the function passed asT.
Example 3: Distributive Conditional Types
type Flatten<T> = T extends (infer U)[] ? U : T;
type Test1 = Flatten<number[]>; // number
type Test2 = Flatten<string[]>; // string
type Test3 = Flatten<boolean>; // boolean
In this example:
- The type
Flattentakes a typeTand checks if it is an array. If it is, it returns the element type (U) of the array; otherwise, it returnsTitself. Flatten<number[]>resolves tonumber.Flatten<boolean>resolves toboolean.
Best Practices for Using Conditional Types
- Use
extendsfor Type Checks: When you need to test whether a type extends another,extendsis the go-to approach. It allows you to create flexible conditional types that adapt to the structure of the types you work with. - Leverage
inferfor Type Inference:inferis useful when you want to extract types from more complex structures like functions. Use it to capture return types, parameter types, and other dynamic types. - Distributive Conditional Types: When working with union types, be aware that TypeScript applies the conditional logic to each member of the union. This can be used to create powerful, type-safe utilities that handle multiple types flexibly.
- Keep Types Simple: While conditional types offer powerful features, try to keep your type logic simple and understandable. Complex conditional types can make code harder to maintain.
- Avoid Overuse of
never: Be cautious when usingneveras a fallback in conditional types. It can sometimes lead to overly restrictive types, so use it only when you expect that a condition should never be true.
Conclusion
Conditional types in TypeScript are an incredibly powerful feature that provides flexibility and type safety when working with types that depend on other types. By understanding and mastering the use of extends, infer, and distributive conditional types, you can create more dynamic, reusable, and robust code.
Through real-world examples and best practices, we’ve demonstrated how these features allow you to build complex type logic, making your TypeScript applications more precise and type-safe.
As you continue to explore TypeScript’s advanced types, conditional types will become an essential part of your toolkit, enabling you to write more flexible and intelligent code.

