Table of Contents
- Introduction
- What Are Conditional Types in TypeScript?
- Basic Syntax of Conditional Types
- Understanding
extends
in Conditional Types - The
infer
Keyword in Conditional Types - Distributive Conditional Types
- Practical Examples
- Example 1: Using
extends
in Conditional Types - Example 2: Using
infer
for 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:
T
is the type being checked.U
is the type being tested against.X
is the result if the conditionT extends U
istrue
.Y
is the result if the conditionT extends U
isfalse
.
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"
becausestring
extendsstring
.IsString<number>
resolves to"No"
becausenumber
does 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"
becausestring
does 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:
ReturnTypeOf
checks if the typeT
extends 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 asvoid
because the function doesn’t return anything. Func3
returns 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 bothstring
andnumber
. 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"
becauseboolean
extendsboolean
.IsBoolean<number>
resolves to"No"
becausenumber
does 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:
FunctionReturnType
usesinfer
to 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
Flatten
takes a typeT
and checks if it is an array. If it is, it returns the element type (U
) of the array; otherwise, it returnsT
itself. Flatten<number[]>
resolves tonumber
.Flatten<boolean>
resolves toboolean
.
Best Practices for Using Conditional Types
- Use
extends
for Type Checks: When you need to test whether a type extends another,extends
is the go-to approach. It allows you to create flexible conditional types that adapt to the structure of the types you work with. - Leverage
infer
for Type Inference:infer
is 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 usingnever
as 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.