TypeScript Advanced Types — Conditional Types
Tags: webdev, programming, javascript, typescript
Published: 2021-01-23 20:03:16

**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 from `T` those types that are assignable to `U`.
*   `Extract<T, U>` – extract from `T` those types that are assignable to `U`.
*   `NonNullable<T>` – exclude `null` and `undefined` from `T`.
*   `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)` .