TypeScript Type Manipulation : Conditional Types

TypeScript Type Manipulation : Conditional Types

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

Overview of Conditional Types

Introduction

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 = T extends { length: number } ? true : false;

type StringOrArray = T extends string ? string : T extends Array ? Array : never;

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 = true;

const stringOrArray: StringOrArray = 'example';

const array: number[] = [1, 2, 3];

const stringFromArray: StringOrArray = 'example';

const arrayFromString: StringOrArray = ['example', 'example'];

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 = T extends { age: number } ? { [K in keyof T]: K extends ‘age’ ? T[K]? : T[K] } : T;

“`

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(arr: T[], condition: (v: T) => boolean): T[] {

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 = “name”;

// Error: Type ‘age’ is not assignable to type ‘StringKeys

const otherStringKeys: StringKeys = “age”;

“`

In this example, the type `StringKeys` is defined using a mapping type `keyof T`. The type `T[K] extends string ? K : never` checks if the value of property `K` in `T` is of type `string`. If it is, then `K` is included in the resulting type; otherwise, it is excluded using the `never` keyword. The resulting type is then accessed using the `keyof T` operator.

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

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.