Check out my books on Amazon at https://www.amazon.com/John-Au-Yeung/e/B08FT5NT62
Subscribe to my email list now at http://jauyeung.net/subscribe/
TypeScript has many advanced type capabilities and which makes writing dynamically typed code easy. It also facilitates the adoption of existing JavaScript code since it lets us keep the dynamic capabilities of JavaScript while using the type-checking capability of TypeScript. There are multiple kinds of advanced types in TypeScript, like intersection types, union types, type guards, nullable types, and type aliases, and more.
In this article, we’ll look at conditional types.
Conditional Types
Since TypeScript 2.8, we can define types with conditional tests. This lets us add types to data that can have different types according to the condition we set. The general expression for defining a conditional type in TypeScript is the following:
T extends U ? X : Y
T extends U
describes the relationship between the generic types T
and U
. If T extends U
is true
then the X
type is expected. Otherwise, the Y
type is expected. For example, we can use it as in the following code:
interface Animal {
kind: string;
}
interface Cat extends Animal {
name: string;
}
interface Dog {
name: string;
}
type CatAnimal = Cat extends Animal ? Cat : Dog;
let catAnimal: CatAnimal = <Cat>{
name: 'Joe',
kind: 'cat'
}
In the code above, we created the CatAnimal
type alias which is set to the Cat
type if Cat extends Animal
. Otherwise, it’s set to Dog
. Since Cat
does extend Animal
, the CatAnimal
type alias is set to the Cat
type.
This means that in the example above if we change <Cat>
to <Dog>
like we do in the following code:
interface Animal {
kind: string;
}
interface Cat extends Animal {
name: string;
}
interface Dog {
name: string;
}
type CatAnimal = Cat extends Animal ? Cat : Dog;
let catAnimal: CatAnimal = <Dog>{
name: 'Joe',
kind: 'cat'
}
We would get the following error message:
Property 'kind' is missing in type 'Dog' but required in type 'Cat'.(2741)
This ensures that we have the right type for catAnimal
according to the condition expressed in the type. If we want to Dog
to be the type for catAnimal
, then we can write the following instead:
interface Animal {
kind: string;
}
interface Cat {
name: string;
}
interface Dog extends Animal {
name: string;
}
type CatAnimal = Cat extends Animal ? Cat : Dog;
let catAnimal: CatAnimal = <Dog>{
name: 'Joe'
}
We can also have nested conditions to determine the actual type from multiple conditions. For example, we can write:
interface Animal {
kind: string;
}
interface Bird {
name: string;
}
interface Cat {
name: string;
}
interface Dog extends Animal {
name: string;
}
type AnimalTypeName<T> =
T extends Animal ? Cat :
T extends Animal ? Dog :
T extends Animal ? Bird :
Animaltype t0 = AnimalTypeName<Cat>;
type t1 = AnimalTypeName<Dog>;
type t2 = AnimalTypeName<Animal>;
type t3 = AnimalTypeName<Bird>;
Then we get the following types for the type alias t0
, t1
, t2
, and t3
:
type t0 = Animal
type t1 = Cat
type t2 = Cat
type t3: Animal
The exact doesn’t have to be chosen immediately, we can also have something like:
interface Foo {}
interface Bar extends Foo {
}
function bar(x) {
return x;
}
function foo<T>(x: T) {
let y: T extends Foo ? string : number = bar(x);
let z: string | number = y;
}
foo<Bar>(1);
foo<Bar>('1');
foo<Bar>(false);
As we can see we can pass in anything into the foo
even though we have the conditional types set. This is because the actual type in the type condition hasn’t been chosen yet., so TypeScript doesn’t make any assumption about what we can assign to the variables in the foo
function.
Distributive Conditional Types
Conditional types are distributive. If we have multiple conditional types that can possibly extend one type as we have in the following code:
interface A {}
interface B {}
interface C {}
interface D {}
interface X {}
interface Y {}type TypeName = (A | B | C) extends D ? X : Y;
Then the last line is equivalent to:
(A extends D ? X : Y) | (B extends D ? X : Y) | (C extends D ? X : Y)
For example, we can use it to filter out types with various conditions. For example, we can write:
type Diff<T, U> = T extends U ? never : T;
To remove types from T
that are assignable to U
. If T extends U
, then the Diff<T, U>
type is never
, which means that we can assign anything to it, otherwise it takes on the type T
. Likewise, we can write:
type Filter<T, U> = T extends U ? T : never;
to remove types from T
that aren’t assignable to U
. In this case, if T extends U
, then the Filter type is the same as the T
type, otherwise, it takes on the never
type. For example, if we have:
type Diff<T, U> = T extends U ? never : T;
type TypeName = Diff<string| number | boolean, boolean>;
Then TypeName
has the type string | number
. This is because Diff<string| number | boolean, boolean>
is the same as:
(string extends boolean ? never : string) | (number extends boolean ? never: number) | (boolean extends boolean ? never: boolean)
On the other hand, if we write:
type Filter<T, U> = T extends U ? T : never;
type TypeName = Filter<string| number | boolean, boolean>;
Then TypeName
has the boolean
type. This is because Diff<string| number | boolean, boolean>
is the same as:
(string extends boolean ? string: never) | (number extends boolean ? number: never) | (boolean extends boolean ? boolean: never)
Predefined Conditional Types
TypeScript 2.8 has the following predefined conditional types, They’re the following:
-
Exclude<T, U>
– excludes fromT
those types that are assignable toU
. -
Extract<T, U>
– extract fromT
those types that are assignable toU
. -
NonNullable<T>
– excludenull
andundefined
fromT
. -
ReturnType<T>
– get the return type of a function type. -
InstanceType<T>
– get the instance type of a constructor function type.
Since TypeScript 2.8, we can define types with conditional tests. The general expression for defining a conditional type in TypeScript is T extends U ? X : Y
. They’re distributive, so (A | B | C) extends D ? X : Y;
is the same as (A extends D ? X : Y) | (B extends D ? X : Y) | (C extends D ? X : Y)
.