An Introduction to Conditional Types in TypeScript
Tags: typescript, javascript, webdev, tutorial
Published: 2022-08-07 16:42:18

Conditional types in TypeScript give us the ability to define certain types based on logic, just like we do in other aspects of our code. They are a useful tool in [defining types](https://fjolt.com/article/typescript-creating-custom-types) in TypeScript.

They take a familiar format, in that we write them like `condition ? ifConditionTrue : ifConditionFalse` - which is a format already used everywhere in TypeScript and Javascript. Let's look at how they work.

## How Conditional Types work in TypeScript

Let's look at a simplistic example to understand how this works. Here, a value could be the user's date of birth (DOB) or age. If it's a date of birth, then the type should be string - but if it's an age, it should be a number. We'll define three types: `Dob`, `Age`, and `UserAgeInformation`.
```typescript
type Dob = string;
type Age = number;
type UserAgeInformation<T> = T extends number ? number : string;
```

So as mentioned, `Dob` will be a `string`, like `12/12/1942`, and `Age`, should be a `number`, like `96`. 

When we defined `UserAgeInformation`, we wrote it like this:
```typescript
type UserAgeInformation<T> = T extends number ? number : string;
```

Where `T` is an argument for `UserAgeInformation`. We can pass any type in here. Then we say, if `T extends number`, then the type is `number`. Otherwise, it's `string`. What we're essentially saying here, is if `T` is of type `number`, then `UserAgeInformation` should be a `number`. 

We can then pass `Age` into `userAgeInformation` if we want it to be a number, and `Dob` in, if we want it to be a string:
```typescript
type Dob = string;
type Age = number;
type UserAgeInformation<T> = T extends number ? number : string;

let userAge:UserAgeInformation<Age> = 100;
let userDob:UserAgeInformation<Dob> = '12/12/1945';
```

## Combining Conditional Types with keyof

We can take this a step further by checking if `T` extends an object. For example, let's say we run a business which has two types of customers: `Horse`s, and `User`s. Although a `User` has an address, a `Horse` typically only has a location. For each, we have different address formats, as shown below:
```typescript
type User = {
    age: number,
    name: string,
    address: string
}

type Horse = {
    age: number,
    name: string
}

type UserAddress = {
    addressLine1: string,
    city: string,
    country: string,
}

type HorseAddress = {
    location: 'farm' | 'savanna' | 'field' | 'other'
}
```

In the future, we may also have other types of customers, so we can check generically if `T` has the property `address`. If it does, use the `UserAddress`. Otherwise, use the `HorseAddress` as the final type:
```typescript
type AddressComponents<T> = T extends { address: string } ? UserAddress : HorseAddress

let userAddress:AddressComponents<User> = {
    addressLine1: "123 Fake Street",
    city: "Boston",
    country: "USA"
}

let horseAddress:AddressComponents<Horse> = {
    location: 'farm'
}
```

When we say `T extends { address: string }`, we check if `T` has the property `address` on it. If it does, we'll use `UserAddress`. Otherwise, we can default to `HorseAddress`.


## Using T in conditional returns

We can even use `T` itself in the conditional returns. In this example, since `T` is defined as `User` when we call it (`UserType<User>`), `myUser` is of type `User`, and requires the fields defined in that type (`age`, `name`, `address`):
```typescript
type User = {
    age: number,
    name: string,
    address: string
}

type Horse = {
    age: number,
    name: string
}

type UserType<T> = T extends { address: string } ? T : Horse

let myUser:UserType<User> = {
    age: 104, 
    name: "John Doe",
    address: "123 Fake Street"
}
```

### Union Types when using T in type outputs
If we were to pass a union type in here, each will be tested separately. For example, let's say we did the following: 
```typescript
type UserType<T> = T extends { address: string } ? T : string
let myUser:UserType<User | Horse> = {
    age: 104, 
    name: "John Doe",
    address: "123 Fake Street"
}
```

`myUser`, above, actually becomes of type `User | string`. That's because although `User` passes the conditional check, `Horse` does not - so it returns string. 

If we modify T in some way (like make it an array). All `T` values will be modified individually. For example, take the following example:
```typescript
type User = {
    age?: number,
    name: string,
    address?: string
}
type Horse = {
    age?: number,
    name: string
}
type UserType<T> = T extends { name: string } ? T[] : never;
//   ^ -- will return the type arguement T as T[], if T contains the property `name` of type `string`
let myUser:UserType<User | Horse> = [{ name: "John" }, { name: "Horse" }]
//  ^ -- becomes User[] | Horse[], since both User and Horse have the property name
```

Here, we've simplified `User` and `Horse` to only have the required property `name`. In our conditional type, both types contain the property `name`. As such, both return true, and the type returned is `T[]`. Since both return true, `myUser` has a type of `User[] | Horse[]`, so we can simply provide an array of objects containing the name property.

This behaviour is usually fine, but you might want to instead return an array of either `User` or `Horse` in some circumstances. In that case, where we want to avoid the distributing of types like this, we can add brackets around `T` and `{ name: string }`:
```typescript
type User = {
    age?: number,
    name: string,
    address?: string
}
type Horse = {
    age?: number,
    name: string
}
type UserType<T> = [T] extends [{ name: string }] ? T[] : never;
//   ^ -- here, we avoid distributing the types, since T and { name: string } are in brackets
let myUser:UserType<User | Horse> = [{ name: "John" }, { name: "Horse" }]
//  ^ -- that means the type is slightly different now - it is (User | Horse)[]
```

By using the square brackets, our type has now been converted to `(User | Horse)[]`, rather than `User[] | Horse[]`. This can be useful in some specific circumstances, and is a complexity about conditional types which is good to remember.

## Inferring types with conditional types
We can also use the `infer` keyword when using conditional types. Suppose we have two types, one for an array of numbers, and another for an array of strings. In this simple case, `infer` will infer what the types of each item in the array is, and return the correct type:
```typescript
type StringArray = string[];
type NumberArray = number[];
type MixedArray = number[] | string[];
type ArrayType<T> = T extends Array<infer Item> ? Item : never;
let myItem1:ArrayType<NumberArray> = 45
//  ^ -- since the items in `NumberArray` are of type `number`, the type of `myItem` is `number`.
let myItem2:ArrayType<StringArray> = 'string'
//  ^ -- since the items in `StringArray` are of type `string`, the type of `myItem` is `string`.
let myItem3:ArrayType<MixedArray> = 'string'
//  ^ -- since the items in `MixedArray` can be `string` or `number, the type of `myItem is `string | number`
```

Here, we define a new argument in our conditional type called `Item`, which is the items within the `Array` which `T` extends. Notably, this only works if the type we pass in is an array, since we are using `Array<infer Item>`. 

In cases where `T` is an array, then `ArrayType` returns the type of its items. If `T` is not an array, then `ArrayType` will be of type `never`.

## Conclusion

Conditional types in TypeScript can seem confusing at first, but it's basically just another way to simplify how we write types in some specific circumstances. It's useful to know how it works, should you ever see it in a repository or project somewhere, or for simplifying your own codebase. 

I hope you've enjoyed this guide. If you did, you might also enjoy the article I wrote on the [Record utility type](https://fjolt.com/article/typescript-record-type).