# What are conditional types in Typescript?

_Lets explore one of the most powerful yet confusing features of Typescript, conditional types. What are they? how do you use them?_

By Yonatan Vega · November 26, 2024

> Source: https://fullstack.rocks/article/typescript-conditional-types

![A blue question mark on a scroll stained in blue](https://storage.googleapis.com/fullstack-rocks-media/blue-question-mark-106461a7f2fd.webp)

Conditional typing is perhaps a weird idiom to grasp just by looking it at. Which is why, from my experience, many typescript developers work with it at just a base level,\
since investing time into understanding how to write "proper" type-safe functions is too much of a hassle.

I personally think that if you decide to pick up typescript you have to learn it much differently than you would most other programming languages, instead of trying to just understand its syntax, you have to think about it as if **you are the person dictating the syntax.**

## Simple answer

Conditional types are a way to create types that depend on other types.\
They are a way to create a type that is based on a condition, and can be used to create more type-safe functions.\
It is a way to determine types in an if-else manner.

## From the documentation

```typescript
interface Animal {
  live(): void;
}

interface Dog extends Animal {
  woof(): void;
}

type Example1 = Dog extends Animal ? number : string;
```

_Type hinting works in this article's code blocks, hover to inspect._

Pretty straight forward, using what is basically a `Ternary Operator` you can *ask* the type system to check the type in your condition against another, and depending on the result return another type.

The `extends` keyword is basically a truthfulness operator here, in simple terms it asks:\
Does `Dog extend Animal`? or is `Dog` a type of `Animal`?

- If it does, the typeof `Example1` is a number
- Otherwise, its a string

Based on the basics of polymorphism, we know that since the `Dog` interface does in-fact extend `Animal`, it is an interface that is also a type of `Animal`, which is why\
`type Example1 = number`.

## But what is it good for?

Your mind might be racing to find a use case for such a thing, it might not seem obvious at first, because this is the type system we are talking about.\
We cannot use this for our variables, we cannot use this with JSON data.\
But let me remind you of my previous statement, **with typescript, we are dictating syntax, not just using it.**

## A use case

Say you want to implement a single function that takes either an array or an object and allows you to `map` over the collection.\
An object is not an iterator, but any reader will be lying if they say that they never had a case where they needed to iterate over keys and values of an object.

The most obvious solution might be to just

```typescript
const collection = {"Hello": "World", "foo": 123};
Object.entries(collection).map(([key, value]) => {
  console.log("Key", key)
  console.log("Value", value)
})
```

_Don't forget to inspect the types of key and value_

While it helps plenty, I would like to have the type of the key be inferred, and not just a `string`.\
While it's not a big deal, it is a good practice to have your types as specific as possible.

So why not create a utility function that iterates and manipulates both `Objects` and `Arrays` and gives us proper type hinting? Allowing us to basically `map` over our collections?

## A Javascript implementation

Let start by implementing a version of our function, just as a proof of concept. We will implement the function in `Javascript`, and then convert it to `Typescript`.

```javascript
function map(collection, callback) {
  // If the collection is an array, map over it
  if (Array.isArray(collection)) {
    return collection.map(callback);
  }
  // If the collection is an object, map over its entries, and create a new object from them
  return Object.fromEntries(
    Object.entries(collection).map(
      ([key, value]) => [key, callback(value, key, collection)]
    )
  );
}

const arrMapRes = map([1,2,3, 'bla'], (value, key) => {
  if (value == 1) return Math.random();
  return "Hello";
});

console.log(arrMapRes);
// [0.02386789139876777, "Hello", "Hello", "Hello"]

const objMapRes = map({something: false, more: 'stuff'}, (value, key) => {
  if (value == false) return false;
  return Math.random();
});

console.log(objMapRes);
// {something: false, more: 0.02386789139876777}
```

Now that we know how our `Javascript` implementation looks like, its a matter of converting it to `Typescript`. First we need to think about what we want to achieve.

- We want to be able to iterate over either an `Array` or an `Object`
- We want the correct types for our `keys` and `values` in the callback function
- We want the result type of the function to be based on the return type of our callback

## Typescript implementation.

Enter generics and conditional types.\
We will begin by defining the type of our callback function based on our data type.

```typescript
// Array map callback
type ArrayMapSignature<C extends Array<unknown>, U> = (
  value: C[number],
  key: number,
  collection: C
) => U;

// Object map callback
type ObjectEntriesSignature<C extends Record<string, unknown>, U> = (
  value: C[keyof C],
  key: keyof C,
  collection: C
) => U;
```

There are multiple typescript tricks being used here so let me explain them.

1. `C[number]` is a way to get the type of the array values

    - since `C` is an array, it is indexed by numbers
    - `C[number]` is the type of the values in the array.

2. `C[keyof C]` is a way to get the type of the object values

    - since `C` is an object, it's values are indexed by predefined keys
    - `C[keyof C]` is the type of the values in the object.

3. `C extends DataType` is a way to ensure that the type `C` inherits its type from the `DataType`.

We use another generic here, `U` is the return type of our callback function.

### Conditional types

For our next type we are actually going to use conditional types, which will give us type hinting on the return type of our function.

```typescript
// Conditional return type
type MapperReturnType<C, U> = C extends Array<unknown> ? U[] : Record<string, U>;
```

Note that we are using the same `extends` keyword that we use for our generic types, where we instruct typescript to only allow types that are either an `Array` or an `Object`.

Here, using the same keyword, we are asking typescript to check if the type `C` is an `Array`.\
if not, we assume it is an `Object`, since in our function we only allow these two types.

We again use another generic, `U` is the return type of our callback function.

## The function

Now that we have our types, we can implement our function.

```typescript
// Array map callback
type ArrayMapSignature<C extends Array<unknown>, U> = (
  value: C[number],
  key: number,
  collection: C
) => U;

// Object map callback
type ObjectEntriesSignature<C extends Record<string, unknown>, U> = (
  value: C[keyof C],
  key: keyof C,
  collection: C
) => U;

// Conditional return type
type MapperReturnType<C, U> = C extends Array<unknown> ? U[] : Record<string, U>;

/**
* Map over an array or object
* @param collection The collection to map over
* @param callback The callback function
* @returns The mapped collection
*/
function map<C extends Array<unknown> | Record<string, unknown>, U> (
  collection: C,
  callback: C extends Array<unknown> ? ArrayMapSignature<C, U> : C extends Record<string, unknown> ? ObjectEntriesSignature<C, U> : never
): MapperReturnType<C, U> {
  if (Array.isArray(collection)) {
    return collection.map(
      callback as ArrayMapSignature<unknown[], U>
    ) as MapperReturnType<C, U>;
  }
  return Object.fromEntries(
    Object.entries(collection).map(([key, value]) =>
      [
        key,
        (callback as ObjectEntriesSignature<Record<string, unknown>, U>)(
          value,
          key,
          collection
        )
      ]
    )
  ) as MapperReturnType<C, U>;
}

const arrMapRes = map([1,2,3, 'bla'], (value, key) => {
  if (value == 1) return Math.random();
  return "Hello";
});

console.log(arrMapRes);
// [0.2218412506876788,"Hello","Hello","Hello"]

const objMapRes = map({something: false, more: 'stuff'}, (value, key) => {
  if (value == false) return false;
  return Math.random();
});

console.log(objMapRes);
// {"something": false, "more": 0.0894643929759662}
```

_Make sure to inspect types of the our constants and callback arguments _

At first glance, this function may seem a bit overwhelming, but let me break it down for you.

- We define our function `map` with two generics, `C` and `U`.
- We state that whatever is passed as `collection` must be either an `Array` or an `Object`.
- We define `U` and pass it to `ArrayMapSignature` and `ObjectEntriesSignature`, as the return type for our callbacks.

On line `26` we see our first nested conditional type, which is a bit more advanced.

But why do we need to nest our conditional types?\
After we test if `C` is an `Array`, `C` is assumed an Array (because of the `extends` keyword)\
if it is not, typescript will only remember that `C` was assumed as an `Array` even though the assumption was wrong.\
In that case we need to provide typescript with an alternative, which is why we make another assumption (`extends`) in the `else` clause of the conditional type,\
think about this like an `else if` statement.

Our last `else` statement is the `never` type, which is a way to tell typescript that this should never happen, and if it does, throw an error.

Basically, we use the conditional type to check if the callback function should be an `ArrayMapSignature` or an `ObjectEntriesSignature`

### Type casting

Why do need to still type cast our callback function?\
While its not a good practice to type cast without a good reason, in this case we have a good reason, and we do it carefully.\
Here, for example:

```typescript
if (Array.isArray(collection)) {
  return collection.map(
    callback as ArrayMapSignature<unknown[], U>
  ) as MapperReturnType<C, U>;
}
```

Since we are now deciding what to do with our function using values, typescript does not help us here.\
We need to instead check the type of the collection parameter, which does not help typescript infer the type of the callback function.

But since we know that the type of the collection is an Array, and that our callback signature in this case will have to be `ArrayMapSignature`, we can safely type cast it.

And again with our `MapperReturnType<C, U>` conversion, since the `Array.map` function returns a `U[]`, we can safely type cast it to our `MapperReturnType`,\
the type of which will also be inferred by typescript as `U[]` since our collection in this case is an array.

# Conclusion

This is an advanced example of how you can use conditional types in typescript, and while it may seem a bit overwhelming at first, it is a very powerful tool to have in your toolbox.

Hopefully this article has given you a better understanding of how to use conditional types in typescript, and how you can use them to create more type-safe functions.
