Learn how to define types based on conditions using conditional type expressions in TypeScript. Explore syntax, examples, and practical use cases.
In this section, we will explore one of the more advanced features of TypeScript: conditional types. Conditional types allow us to define types that depend on a condition, making our code more flexible and expressive. This feature is particularly useful when working with complex data structures or when you need to create types that adapt based on other types.
Conditional types in TypeScript are a powerful tool that allows you to create types based on a condition. The basic syntax of a conditional type is as follows:
T extends U ? X : Y
In this expression, T
is the type you are checking, U
is the type you are comparing against, X
is the type that will be used if the condition is true, and Y
is the type that will be used if the condition is false.
Let’s start with a simple example to illustrate how conditional types work:
type IsString<T> = T extends string ? "Yes" : "No";
type Test1 = IsString<string>; // "Yes"
type Test2 = IsString<number>; // "No"
In this example, we define a type IsString
that checks if a given type T
is a string. If T
extends string
, the type resolves to "Yes"
, otherwise, it resolves to "No"
.
One of the unique features of conditional types is that they distribute over union types. This means that if you apply a conditional type to a union type, TypeScript will apply the conditional type to each member of the union individually.
Consider the following example:
type ToArray<T> = T extends any ? T[] : never;
type Test3 = ToArray<string | number>; // string[] | number[]
Here, ToArray
is a conditional type that converts any type T
into an array of T
. When applied to the union type string | number
, it results in string[] | number[]
. This is because the conditional type is applied to each member of the union separately.
Conditional types are not just a theoretical concept; they have practical applications in real-world TypeScript code. Let’s explore some common use cases.
Conditional types can be used to filter types from a union. For example, you might want to extract only the string types from a union of multiple types.
type ExtractString<T> = T extends string ? T : never;
type Test4 = ExtractString<string | number | boolean>; // string
In this example, ExtractString
filters out all types except string
from the union string | number | boolean
.
Another common use case is extracting specific types from a complex structure. For instance, you might want to extract the return type of a function.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Test5 = ReturnType<() => string>; // string
type Test6 = ReturnType<(x: number) => number>; // number
Here, ReturnType
uses the infer
keyword to extract the return type R
from a function type T
.
Let’s dive into some more advanced examples to deepen our understanding of conditional types.
You can chain multiple conditional types to handle more complex scenarios.
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
"unknown";
type Test7 = TypeName<string>; // "string"
type Test8 = TypeName<number>; // "number"
type Test9 = TypeName<boolean>; // "boolean"
type Test10 = TypeName<null>; // "unknown"
In this example, TypeName
checks the type of T
and returns a string representing the type name. If T
is not a string, number, or boolean, it defaults to "unknown"
.
You can combine conditional types with mapped types to transform complex data structures.
type Nullable<T> = {
[K in keyof T]: T[K] extends object ? Nullable<T[K]> : T[K] | null;
};
type Test11 = Nullable<{ a: number; b: { c: string } }>;
// { a: number | null; b: { c: string | null; } | null; }
In this example, Nullable
recursively makes all properties of an object type T
nullable.
To better understand how conditional types work, let’s use a visual aid to represent the flow of a conditional type expression.
graph TD; A[Type T] --> B{T extends U?} B -->|Yes| C[Type X] B -->|No| D[Type Y]
This diagram illustrates the decision-making process of a conditional type. If T
extends U
, the type resolves to X
; otherwise, it resolves to Y
.
Now that we’ve covered the basics and some advanced examples, it’s time to experiment with conditional types on your own. Try modifying the examples above to see how changes affect the resulting types. For instance, you can:
For further reading and deeper dives into conditional types, consider exploring the following resources: