TypeScript is a statically-typed superset of JavaScript that incorporates type annotations and checking to provide enhanced developer experience and catch potential errors at compile-time. One of the powerful features in TypeScript is the ability to manipulate types using conditional types.
Conditional types allow us to define types that depend on other types. They enable us to create sophisticated type transformations and conditional logic based on the shape of existing types. In simpler terms, conditional types enable us to create type constraints and generate type variations based on those constraints.
With conditional types, we can perform complex type operations, such as filtering types, transforming types, and merging types based on certain conditions. This feature gives TypeScript developers the ability to create more flexible and expressive type systems to handle different scenarios and constraints.
In this article, we will explore the power of conditional types in TypeScript and see how they can be used to solve common problems and simplify type definitions. We will cover the syntax and usage of conditional types, and demonstrate examples of their application in real-world scenarios.
Table of Contents
- 1 Overview of Conditional Types
- 2 Using Conditional Types for Type Inference
- 3 Filtering Types with Conditional Types
- 4 Mapping Types with Conditional Types
- 5 Using Mapped Types in Conditional Types
- 6 Creating Complex Types with Conditional Types
- 7 Conditional Types for Union Types
- 8 Conditional Types for Intersection Types
- 9 Leveraging Conditional Types in Generic Functions
- 10 Advanced Techniques with Conditional Types
- 11 FAQ:
- 11.0.1 What are conditional types in TypeScript?
- 11.0.2 How do conditional types work?
- 11.0.3
- 11.0.4 What are some use cases for conditional types?
- 11.0.5 Can I nest conditional types in TypeScript?
- 11.0.6 Are there any limitations or caveats when using conditional types in TypeScript?
- 11.0.7 Where can I learn more about conditional types in TypeScript?
Overview of Conditional Types
Introduction
Conditional types are a powerful feature introduced in TypeScript 2.8 that allow you to create dynamic types based on conditions or constraints. They provide a way to define types that depend on other types, making your code more flexible and expressive.
Syntax
The syntax for conditional types consists of three parts: a condition, a true branch, and a false branch. The condition is an expression that evaluates to either true or false. The true branch is the type that is used if the condition is true, and the false branch is the type that is used if the condition is false.
type Result<T> = T extends string ? 'Success' : 'Failure';
In the above example, the condition is a type constraint that checks if the generic type variable T
extends the string
type. If it does, the type 'Success'
is used; otherwise, the type 'Failure'
is used.
Use Cases
Conditional types are commonly used in generic types to handle different scenarios based on the types of the input arguments. They can be used to create union types, intersection types, mapped types, and more.
Union Types
Conditional types can be used to create union types based on the types of the input arguments. By using the extends
keyword, you can define a condition that checks if a type extends another type, and then use the result of the condition to create a union type.
type Combine<T, U> = T extends U ? never : T | U;
In the above example, the Combine
type takes two type arguments T
and U
. It checks if T
extends U
. If it does, the type is never
, which means the combined type is just U
. If it doesn’t, the combined type is T
or U
.
Mapped Types
Conditional types can also be used to create mapped types, which are types that transform one type into another type based on a given condition. By using the extends
and infer
keywords, you can define a condition that checks if a type extends a specific condition, and then use the result of the condition to transform the type.
type Map<T> = T extends string ? {[Key in T]: number} : never;
In the above example, the Map
type takes a generic type argument T
. It checks if T
extends string
. If it does, it creates a mapped type where each key in T
is mapped to the type number
. If it doesn’t, the resulting type is never
.
Conclusion
Conditional types are a powerful feature in TypeScript that allow you to create dynamic types based on conditions or constraints. They can be used to handle different scenarios and create more flexible and expressive code. By understanding the syntax and use cases of conditional types, you can leverage this feature to its full potential in your TypeScript projects.
Using Conditional Types for Type Inference
Conditional types in TypeScript allow us to create types that depend on a condition. This powerful feature can be used to infer types based on different conditions, making our code more flexible and reusable.
Basic usage
Let’s start with a basic example of using conditional types for type inference. Suppose we have a function called getElement
that takes an argument elementId
. This function should return different types based on the type of the elementId
argument.
function getElement(elementId: string): Element;
function getElement(elementId: number): HTMLInputElement;
function getElement(elementId: any) {
// implementation
}
In the above code, we have defined two different overloads for the getElement
function. The first overload returns an Element
type when the elementId
argument is of type string, and the second overload returns an HTMLInputElement
type when the elementId
argument is of type number.
More complex example
Now, let’s take a look at a more complex example where we use conditional types to infer types based on the properties of an object.
type HasLength
type StringOrArray
In the above code, we have defined two conditional types. The first one, HasLength
, checks if the type T
has a property length
of type number. If it does, it assigns the type true
; otherwise, it assigns the type false
.
The second conditional type, StringOrArray
, checks if the type T
is a string. If it is, it assigns the type string
. If the type T
is an array, it extracts the type of the elements of the array using the [number]
index type and assigns the type Array<T[number]>
. Otherwise, it assigns the type never
.
By using these conditional types, we can now infer the types of different variables based on their properties:
const hasLength: HasLength
const stringOrArray: StringOrArray
const array: number[] = [1, 2, 3];
const stringFromArray: StringOrArray
const arrayFromString: StringOrArray
Conclusion
Conditional types in TypeScript provide a powerful mechanism for type inference. By using conditional types, we can create types that depend on different conditions, making our code more flexible and reusable. Whether it’s inferring types based on function arguments or object properties, conditional types allow us to write more generic and concise code.
Filtering Types with Conditional Types
Introduction
Conditional types in TypeScript allow you to filter types based on certain conditions. With conditional types, you can create more specific types based on the properties or values of another type.
Filtering Types
One of the common use cases of conditional types is filtering types based on certain conditions. This can be useful when you need to create a subset of a type that satisfies specific criteria.
Let’s say you have a type Person
with properties name
and age
:
type Person = {
name: string;
age: number;
};
If you want to create a type that includes only the persons whose age is greater than 18, you can use a conditional type:
type Adult = Filter<Person, { age: number }>;
// Example usage:
const person: Adult = {
name: "John",
age: 25
};
In the above example, Filter
is a conditional type that filters out the properties from the Person
type that match the condition { age: number }
. The resulting type Adult
will only have properties that are of type { age: number }
.
Creating the Filter Type
To create the Filter
conditional type, you can use the following syntax:
type Filter<T, U> = T extends U ? T : never;
In this example, T
is the input type that we want to filter, and U
is the condition type. The conditional expression T extends U ? T : never
checks if T
is assignable to U
. If the condition is true, the type is kept; otherwise, it is filtered out.
Conclusion
Conditional types in TypeScript are a powerful feature that allow you to filter types based on certain conditions. This can be useful when you need to create more specific types that satisfy specific criteria. By utilizing conditional types, you can write more expressive and type-safe code in your TypeScript applications.
Mapping Types with Conditional Types
Conditional types in TypeScript allow you to create complex type transformations based on the characteristics of other types. One of the powerful use cases for conditional types is mapping types, where you can transform one type into another by applying a set of rules.
Mapping Types
In TypeScript, mapping types are a way to transform existing types into new types. They enable you to modify the properties, remove or add new properties, or change the types of existing properties in a type.
Conditional types provide a powerful mechanism for defining mapping types. With conditional types, you can create mappings that depend on the properties or characteristics of the original type.
Conditional Type Mapping
Conditional types use the infer
keyword to extract a type from a union or intersection type. This allows you to create a mapping that depends on the specific values of the original type.
For example, let’s say we have a type Person
with two properties: name
and age
. We can define a conditional type that transforms the original type by adding an optional property based on the age
property:
type Person = {
name: string;
age: number;
};
type AddOptionalProperty<T> = T extends { age: number } ? T & { optionalProperty?: string } : T;
type NewPerson = AddOptionalProperty<Person>;
In this example, the conditional type AddOptionalProperty
checks if the original type has an age
property. If it does, it adds an optional property optionalProperty
of type string
to the new transformed type. If it doesn’t, it returns the original type unchanged.
By applying the AddOptionalProperty
conditional type to the Person
type, we get a new type NewPerson
with the added optionalProperty
if the original Person
has an age
property.
Conclusion
Conditional types in TypeScript provide a powerful mechanism for defining mapping types. With conditional types, you can create complex type transformations that depend on the properties or characteristics of the original type. This allows you to easily modify, add, or remove properties from existing types, creating new and more specific types.
Using Mapped Types in Conditional Types
Conditional types in TypeScript are a powerful feature that allow you to perform different type operations based on a condition. One way to further enhance the functionality of conditional types is by using mapped types. Mapped types allow you to create a new type by transforming each property in an existing type.
What are Mapped Types?
Mapped types are a feature in TypeScript that allow you to create a new type by applying a transformation function to each property in an existing type. Mapped types can be useful when you want to modify an existing type and create a new type based on it.
For example, let’s say you have an interface called Person
with properties name
and age
:
“`typescript
interface Person {
name: string;
age: number;
}
“`
You can create a mapped type that transforms each property to be optional using the Partial
utility type:
“`typescript
type PartialPerson = {
[Key in keyof Person]?: Person[Key];
};
“`
This will create a new type called PartialPerson
with optional properties:
“`typescript
type PartialPerson = {
name?: string;
age?: number;
};
“`
Using Mapped Types in Conditional Types
When combined with conditional types, mapped types can be used to conditionally modify properties in a type based on a condition. For example, let’s say you have a type called Person
with an age
property:
“`typescript
type Person = {
name: string;
age: number;
};
“`
You can create a conditional type that transforms the age
property to be optional if the person is under a certain age:
“`typescript
type OptionalAgePerson
“`
This will create a new type called OptionalAgePerson
that conditionally modifies the age
property:
“`typescript
type OptionalAgePerson
name: string;
age?: number;
};
“`
By using mapped types in conditional types, you can create complex type transformations based on conditions. This allows you to have more control over how your types are modified and used in your code.
Conclusion
Mapped types in conditional types are a powerful feature in TypeScript that allow you to create new types by transforming existing types based on conditions. By combining the functionality of mapped types and conditional types, you can have more control over how your types are modified and used in your applications.
Whether you need to modify individual properties in a type or conditionally modify the entire type, mapped types in conditional types provide a powerful mechanism for creating complex type transformations.
Creating Complex Types with Conditional Types
Conditional types in TypeScript allow us to create complex types that depend on some condition. By using conditional types, we can define type transformations based on the types of input parameters.
Basic Usage
Conditional types in TypeScript are defined using the `infer` keyword. This allows us to infer the type of another type based on some condition. For example, consider the following conditional type:
type TypeName<T> = T extends string
? "string"
: T extends number
? "number"
: T extends boolean
? "boolean"
: "other";
In this example, `TypeName` is a conditional type that depends on the input type `T`. If `T` is `string`, the result will be `”string”`. If `T` is `number`, the result will be `”number”`. If `T` is `boolean`, the result will be `”boolean”`. Otherwise, the result will be `”other”`.
Advanced Usage
Conditional types can also be combined with other types and operators to create more complex types. We can use intersection (`&`), union (`|`), and mapped types to define our desired transformation.
For example, let’s say we have an array of objects with different properties, and we want to transform each object to have an additional property called `id` with a unique identifier. We can achieve this using conditional types:
type WithId<T> = T & { id: string };
type TransformArray<T extends Array<any>> = {
[Key in keyof T]: WithId<T[Key]>
};
In this example, `WithId` is a conditional type that adds the `id` property to the input type `T`. `TransformArray` is another conditional type that uses a mapped type to iterate over each key in the input array type `T` and applies the `WithId` transformation to each element.
Conclusion
Conditional types in TypeScript provide a powerful mechanism for creating complex types that depend on some condition. By using conditional types, we can define type transformations based on the types of input parameters, allowing us to create more flexible and robust code.
Conditional Types for Union Types
In TypeScript, conditional types can also be used with union types to create more complex type manipulations. A union type is a type that represents a value that can be one of several types. When combined with conditional types, union types allow for dynamic type selection based on a condition.
Defining Union Types
To define a union type in TypeScript, you can use the pipe character (|) between the types you want to include in the union. For example:
type MyUnion = string | number | boolean;
In this case, the type MyUnion can represent a value that can be either a string, a number, or a boolean.
Conditional Types with Union Types
Conditional types with union types allow us to define different types based on a condition. This can be useful in situations where we want to perform different operations or apply different constraints depending on the type of the value.
For example, let’s say we have a function that takes an argument of a union type, and we want to perform different operations depending on whether the argument is a string or a number:
function processValue(value: string | number): void {
if (typeof value === 'string') {
// Do something with a string value
} else {
// Do something with a number value
}
}
In this example, the type of the value argument is string | number. Inside the function, we use the typeof operator to check the type of the value and perform different operations accordingly.
Using Conditional Types with Union Types
Conditional types can also be used to dynamically infer the return type of a function based on the type of the argument. This can be achieved using the conditional operator (? 🙂 inside the return type annotation.
type ReturnType<T> = T extends string ? string[] : number[];
function processValue(value: string): ReturnType<string> {
return [value];
}
function processValue(value: number): ReturnType<number> {
return [value];
}
In this example, we define a conditional type ReturnType<T> that returns a string array if the type T is a string, and a number array if the type T is a number. We then use this conditional type as the return type annotation for the processValue function to dynamically infer the return type based on the type of the argument.
Conclusion
Conditional types for union types in TypeScript provide a powerful mechanism for dynamically selecting types based on conditions. This allows for more flexible type manipulations and can be especially useful in scenarios where different operations need to be performed based on the type of the value.
Conditional Types for Intersection Types
Conditional types in TypeScript are powerful tools that allow the type system to make decisions based on the types of other values. They are used to create types that depend on a condition evaluated at the type level.
One common use case for conditional types is when working with intersection types. Intersection types allow you to combine multiple types into one. When using conditional types in conjunction with intersection types, you can create types that have different properties or behavior depending on certain conditions.
Example:
type Foo = { a: number };
type Bar = { b: string };
type A = Foo & Bar;
In the example above, we define two types Foo
and Bar
and then intersect them using the &
operator to create a new type A
. The resulting type A
will have both properties from Foo
and Bar
.
Now let’s introduce a conditional type to control the behavior of the intersection:
type Foo = { a: number };
type Bar = { b: string };
type A<T> = T extends string ? Foo : Bar;
type B = A<string>; // Type is Foo
type C = A<number>; // Type is Bar
In the updated example, we define a conditional type A<T>
that takes a generic type parameter T
. If T
extends string
, the type is set to Foo
; otherwise, it is set to Bar
. By passing different types as the generic parameter, we can control the behavior of the conditional type, which in turn affects the resulting intersection type.
By using conditional types for intersection types, you can create more flexible and dynamic type definitions that adapt to different conditions. This can be particularly useful when working with complex data structures or when building generic utility types.
Leveraging Conditional Types in Generic Functions
In TypeScript, conditional types allow you to create more flexible, powerful generic functions. The ability to conditionally choose types based on other types can greatly enhance the versatility and usefulness of your code.
What are Conditional Types?
Conditional types in TypeScript allow you to provide different type options based on a condition. They are denoted using the conditional operator `?`. Conditional types are evaluated by the compiler and resolve to one of two branch types based on whether the condition is true or false.
Using Conditional Types in Generic Functions
Conditional types can be extremely useful in creating generic functions that can handle different types of input and output. By leveraging conditional types, you can create more flexible and reusable code that adapts to varying scenarios.
For example, let’s say you have a generic function called `filter` that filters out elements from an array based on a given condition. You want this function to work with both arrays of numbers and arrays of strings. Here’s how you can achieve this using a conditional type:
“`typescript
function filter
return arr.filter(condition);
}
type NumberArray = number[];
type StringArray = string[];
const numbers: NumberArray = [1, 2, 3, 4, 5];
const strings: StringArray = [“apple”, “banana”, “cherry”];
const filteredNumbers = filter(numbers, (v) => v % 2 === 0); // [2, 4]
const filteredStrings = filter(strings, (v) => v.startsWith(“a”)); // [“apple”]
“`
In this example, the `filter` function uses a conditional type to conditionally infer the type of the array based on the input. The type parameter `T` represents the element type of the array. It is then used as the input type for the `condition` function and to specify the return type of the `filter` function.
Creating More Advanced Conditional Types
Conditional types can also be combined with other type operators, such as `keyof` and `infer`, to create more advanced and powerful type conditions.
For example, let’s say you have an interface called `Person` with two properties: `name` and `age`. You want to create a conditional type that extracts the keys of this interface whose values are of type `string`. Here’s how you can achieve this using `keyof` and conditional types:
“`typescript
interface Person {
name: string;
age: number;
}
type StringKeys
[K in keyof T]: T[K] extends string ? K : never;
}[keyof T];
const person: Person = {
name: “John”,
age: 30,
};
const stringKeys: StringKeys
// Error: Type ‘age’ is not assignable to type ‘StringKeys
const otherStringKeys: StringKeys
“`
In this example, the type `StringKeys
Conclusion
Conditional types in TypeScript provide a powerful tool for creating more versatile and reusable code. By leveraging conditional types in generic functions, you can handle different types of input and output, resulting in more flexible and adaptable code.
Advanced Techniques with Conditional Types
1. Using the ‘infer’ keyword
The ‘infer’ keyword allows developers to extract a type from another type within a conditional type. It is often used in conjunction with the ‘extends’ keyword to create more complex type conditions.
For example, consider the following conditional type:
type ExtractReturnType
= T extends (...args: any[]) => infer R ? R : never;
In this example, the ‘ExtractReturnType’ type takes in a generic type parameter ‘T’ which is expected to be a function. If ‘T’ matches the function type pattern, the inferred return type ‘R’ is extracted and used as the result type. Otherwise, the type ‘never’ is returned.
2. Union and Intersection of Conditional Types
Conditional types can be combined using union (‘|’) and intersection (‘&’) operators to create more complex type conditions.
For example, consider the following conditional type:
type CombineTypes
= T extends string ? (U extends string ? T & U : T) : U;
In this example, the ‘CombineTypes’ type takes in two generic type parameters ‘T’ and ‘U’. If ‘T’ is a string, it checks if ‘U’ is also a string. If both ‘T’ and ‘U’ are strings, it returns the intersection type ‘T & U’. Otherwise, it returns ‘T’ as the result type. If ‘T’ is not a string, it returns ‘U’ as the result type.
3. Conditional Type Inference
Conditional types can be used to infer types in various scenarios. One such scenario is the inference of function argument types based on the return type of a function.
For example, consider the following conditional type:
type InferArgumentType
= T extends (arg: infer R) => any ? R : never;
In this example, the ‘InferArgumentType’ type takes in a generic type parameter ‘T’ which is expected to be a function. If ‘T’ matches the function type pattern, the inferred argument type ‘R’ is extracted and used as the result type. Otherwise, the type ‘never’ is returned.
4. Mapping over Union Types
Conditional types can be used to map over union types to transform each type within the union.
For example, consider the following conditional type:
type MapToPromise
= T extends any ? Promise : never;
In this example, the ‘MapToPromise’ type takes in a generic type parameter ‘T’ and maps over each type within ‘T’. Each type is wrapped in a ‘Promise’ type. If ‘T’ is a union type, the result is a union of ‘Promise’ types for each individual type within ‘T’.
5. Distributive Conditional Types
Distributive conditional types distribute over union types, allowing type transformations to apply to each constituent type within the union.
For example, consider the following conditional type:
type ToArray
= T extends any ? T[] : never;
In this example, the ‘ToArray’ type takes in a generic type parameter ‘T’. If ‘T’ is a union type, the result is a union of ‘T[]’ types for each individual type within ‘T’. This can be useful when working with mapped types and union types together.
Conclusion
Conditional types provide powerful tools in TypeScript for creating flexible and reusable types. By using techniques such as using the ‘infer’ keyword, combining types with union and intersection operators, inferring types, mapping over union types, and utilizing distributive conditional types, developers can create advanced types that cater to specific needs in their applications.
FAQ:
What are conditional types in TypeScript?
Conditional types in TypeScript are a feature that allows you to perform type-level computations and create more flexible and reusable types based on conditions.
How do conditional types work?
Conditional types work by using a type-level condition that checks a given type and produces a new type based on the result of the condition. They use the `extends` keyword to define the condition and the `?` operator to specify the type to be used if the condition is true.
What are some use cases for conditional types?
Conditional types can be used in various scenarios, such as creating type guards, conditional mapping of types, or enforcing type constraints based on conditions. They provide a way to express complex type relationships and create more flexible and reusable type definitions.
Can I nest conditional types in TypeScript?
Yes, you can nest conditional types in TypeScript. This allows you to create more complex type computations and build type definitions based on multiple conditions.
Are there any limitations or caveats when using conditional types in TypeScript?
Yes, there are a few limitations and caveats when using conditional types in TypeScript. For example, conditional types cannot be recursive and cannot depend on infinite unions or intersections of types. Additionally, conditional types can only operate on the shape of a type, not on its values.
Where can I learn more about conditional types in TypeScript?
You can learn more about conditional types in TypeScript by referring to the official TypeScript documentation. The documentation provides detailed explanations, examples, and best practices for working with conditional types.