Conditional types in TypeScript help you to create types depending on another type, by checking if that type satisfies some condition.
In their simplest form, they take the format of:
MyType extends SomeType ? IfTrueType : IfFalseType;
Here’s a simple example, checking if a type can be converted to a string.
interface Loggable {
toString(): string;
}
type Log<T> = T extends Loggable ? T : never;
type SomePrimitiveTypes = number | string | boolean;
type LoggablePriminitives = Log<SomePrimitiveTypes>; //number | string
Here I’m looking for which of these primitive types in my type union have a toString() method. We can do this using our conditional type. By setting the false type to never, if the type evaluates to false, it gets removed from the type union.
The end result is we get “number | string”, since boolean is the only one that doesn’t have a toString() method.
Conditional Type Unions
In the previous example, you might have noticed TypeScript has done something clever and iterated through the types in the type union. There is however a way to avoid this:
Here’s another example, using a conditional type to turn something into an array if it isn’t already.
type ToArray<T> = T extends unknown[] ? T : T[];
(I’ve used unknown[] here, since it doesn’t actually matter what is in the array)
If we use this with a normal primitive, we get what we expect:
type t1 = ToArray<number>; //number[]
But with a type union, our logic gets distributed over each type in the union.
type t2 = ToArray<string | number>; // string[] | number[]
We can change this by putting square brackets over the items on the left and right of the extends, which tells TypeScript we just want to take this type as-is.
type ToArray<T> = [T] extends [unknown[]] ? T : T[];
type t1 = ToArray<number>; //number[]
type t2 = ToArray<string | number>; //(string | number)[]
Infer
The infer keyword works in a type as a sort of placeholder variable, storing the type you infer to use later. Let’s implement a Flatten type, to unwrap the type from an array.
type FlattenArray<T> = T extends (infer R)[] ? R : T;
Infer R tells TypeScript to keep track of the type of R, so we can then return it in our conditional. For the false case, this means we have something that isn’t an array, and I’ve chosen to just return the same thing, i.e. doing nothing.
We can use it like this:
type ImANumber = FlattenArray<number[]>; // number
type ImAStringArray = FlattenArray<string[][]>; // string[]
Typescript gets the type of the items in the array if there is one, and stores it in R, which we then return.
If you get rid of the infer keyword:
type FlattenArray<T> = T extends (R)[] ? R : T;
We get an error, since TypeScript doesn’t know where the type R should come from. The infer keyword tells TypeScript to take the type that comes, and store it in R.
Conclusion
Thanks for reading this introduction to conditional types in TypeScript. They’re an extremely powerful tool that’s helpful for things from creating types from promises, unwrapping types from more complex types, to some of the crazier things implemented in TypeScript. Let me know in the comments if you liked this article, or any cool examples how you’ve used conditional types in your projects.
💬 Leave a comment