Conditional types allow developers to use conditions when introducing their types. It is a bit of an advanced feature in TypeScript, and many developers do not use it in day-to-day work. But knowing these features can certainly help you to write better code. In this blog, I will introduce you to conditional types in TypeScript.
Conditional types are non-uniform type mappings. In other words, they are type transformations that differ depending on a condition. Let’s take a look at a code snippet.
interface Animal { } interface Dog extends Animal { } type Ex1 = Dog extends Animal ? number : string; //type of Ex1 = number type Ex2 = RegExp extends Animal ? number : string; // type of Ex2 = string
Here, types Ex1 and Ex2 are decided by the condition of whether they are extending the type Animal or not. As the type Dog is extending the Animal type, the condition evaluates as true, and the Ex1 type is evaluated to the type number. For Ex2, since the RegExp type is not extending the Animal type, the condition evaluates as false. Therefore, the Ex2 type is evaluated to the type string.
As mentioned, conditional types are similar to the ternary operator, which we’ve all used in JavaScript. Conditional types allow developers to decide the type of a certain variable or the method’s return type based on a condition.
However, using conditions and types together might confuse you if you haven’t used them before. Therefore, let me help you understand conditional types with a simple example. First, let’s take a look at this simple function.
function createLabel(label) { return label && label.replace(/\s/g, "_"); }
Here, the createLabel method takes a text label as an input parameter. The createLabel() function returns null if the label value equals null. If not, it replaces all the white spaces in the label with underscores and returns the label. Although this function looks straightforward with no issues, TypeScript may throw an error saying that the value could be null.
For example, if you invoke the createLabel() function with createLabel(“foo and poo”).toUpperCase(), it will throw an error since TypeScript is unable to comprehend the type of input.
To avoid this, we can use overloaded functions.
function createLabel(label:string): string; function createLabel(label:null): null; function createLabel(label: string | null): string | null;
Here, the function overloads resolve the previously discussed issue. But these function overloads introduce a new set of problems. For example, if they’re used in a library, having to sort among these function overloads every time this function is called can be cumbersome and would decrease the library’s performance. On the other hand, if the requirement changes and we need to add another variable to the same function, the number of function overloads we need to write will grow exponentially.
So, we need another solution for this issue, and that’s where the conditional types come into play.
With conditional types, the createLabel() function will look like the following.
function createLabel<T extends string | null>(label: T): T extends string ? string : null { return (label ? label.replace(/\s/g, "_") : null) as any; }
Note: Here, I have cast the function’s return value as any. This is an open issue in TypeScript, and the only way to get the benefit of using conditional operators is to cast the return this way.
In the previous function, I introduced a new syntax for the types, which works similarly to the ternary operator. If the condition is true, it will take the first type; if not, it will take the second type. This implementation avoids the previous problems we had with the function overloads, as it can evaluate the type in the runtime.
In conditional types, the most important syntax is the extends keyword, as it decides which type of condition should finally be evaluated. Therefore, understanding how the extends keyword works will help in better understanding the conditional types.
Let’s focus on understanding the extends keyword.
Take a look at the following example of a conditional type.
A extends B ? A : B
This condition will be true if A extends B. This means that the value of type A should be assignable to a variable in type B. Therefore, the extends keyword is actually checking whether A is assignable to type B. Based on this condition, the conditional type will decide which type it should finally evaluate.
Other than being capable of evaluating the type based on a condition, conditional types have a few other exciting features that make them more attractive.
Condition types can handle ambiguities with the help of the infer keyword. We can use the infer keyword after the extends keyword to infer types from the true conditional branch.
Let’s take a look at the following example.
type Box<T> = T extends Array<infer A> ? A : never; type Books = Box<string[]> // type string
In this example, the type of Books is a string, since the generic is a string array. If we change the generic to any type, which is not an array, the type of Books will be never. Conditional types are capable of gracefully handling such ambiguities in types.
Another feature of conditional types is their distribution capability when a union type is given while working with generics. This might sound confusing, so let’s look at an example.
type Apple = {}; type Banana = {}; type Orange = {} type Fruits = Apple | Banana | Orange; type Box<T> = T extends any ? T[] : never; type FruitBox = Box<Fruits>; //Apple[] | Banana[] | Orange[]
Conditional types can identify the underlying types of the union, and the conditional type be applied to each member of the union recursively.
type FruitBox = Box<Apple> | Box<Banana> | Box<Orange>
Then, it will apply the conditional type to each type separately.
type FruitBox = Apple[] | Banana [] | Orange[];
Conditional types might not be a feature used in daily work. But it is a very powerful feature that TypeScript introduced to handle types with conditions dynamically. Using conditional types when implementing libraries and handling APIs can help developers improve their code base.
I hope you have found this article helpful and thank you for reading it!
Syncfusion’s Essential JS 2 is the only suite you will ever need to build an app. It contains over 65 high-performance, lightweight, modular, and responsive UI components in a single package. Download a free trial to evaluate the controls today.
If you have any questions or comments, you can contact us through our support forums, support portal, or feedback portal. We are always happy to assist you!