Using Conditional Types in TypeScript
Tags: typescript, tutorial, programming, types
Published: 2023-06-27 01:27:52

---
title: Using Conditional Types in TypeScript
published: true
date: 2023-05-31 00:00:00 UTC
tags: 
  - typescript
  - tutorial
  - programming
  - types
canonical_url: https://www.susanpotter.net/software/using-conditional-types-in-typescript/
---

# Table of Contents

1.  [A High-Level Understanding](#org48ad424)
2.  [When to use Conditional Types](#orgf8fb829)
3.  [But wait, we didn&rsquo;t use `UpdateObject<T, U>` anywhere](#org107a656)
4.  [Embrace the Power of Conditional Types but only when you really need to! :)](#org657ff19)

Welcome, fellow developer. Today we embark on a [hopefully] helpful journey into the world of conditional types via TypeScript! I hope to impress upon you how we can levereage conditional types to offer improved type safety especially in cases where the type of an associated piece of data can be computed from the type of the original data that is polymorphic in nature.


<a id="org48ad424"></a>

## A High-Level Understanding

Conditional types are like shape-shifters, capable of dynamically transforming and adapting types based on varying conditions. They bestow upon the developer the gift of creating flexible type mappings without compromising on how well-typed our definitions are, thus empowering our code with expressiveness and resilience.

In this introductory section I will show the mechanics of how to describe conditional types in TypeScript itself even if the examples are trivial and not useful so that I am not distracted explaining the syntax in more useful and involved examples later on.

```typescript
    import { option } from 'https://cdn.skypack.dev/fp-ts?dts';
```

Here I am just importing the `Option<T>` parameterized type from my trusted friend `fp-ts`,
a library I tend to use in TypeScript codebases when I am able. If you are unfamiliar with this type think of this as an alternative way of encoding a nullable value in a more type safe way.

```typescript
    type NumberValidator = {
      minValue: option.Option<number>;
      maxValue: option.Option<number>;
    };
    
    type StringValidator = {
      minLength: option.Option<number>;
      maxLength: option.Option<number>;
      contains: option.Option<RegExp>;
      matches: option.Option<RegExp>;
    };
    
    type NoopValidator = {};
```

We start of by defining the type `NumberValidator`, which represents a validator for numeric values, and the `StringValidator` type, which is a validator for string values. Both types have specific properties that define their validation rules.

The `NumberValidator` type has `minValue` and `maxValue` properties, each wrapped in an `Option<number>` type from `fp-ts`. These options can hold either a number value or no value at all.

the `StringValidator` type has `minLength`, `maxLength`, `contains`, and `matches` properties, all wrapped in `Option<T>` types. These options allow us to define constraints such as minimum and maximum lengths, required patterns, and more.

We also have a `NoopValidator` type, which represents a validator that doesn&rsquo;t perform any specific validations. It&rsquo;s essentially an empty object, signaling that no validation rules are applicable.

```typescript
    const slugValidator: Validator<string> = {
      minLength: option.some(1),
      maxLength: option.some(10),
      matches: option.some(/^[a-z\-]+$/),
      contains: option.none,
    };
    
    const titleValidator: Validator<string> = {
      minLength: option.some(1),
      maxLength: option.some(64),
      matches: option.none,
      contains: option.none,
    };
    
    // age validator for purchasing alcohol in any sane country
    const ageValidator: Validator<number> = {
      minValue: option.some(18),
      maxValue: option.some(149),
    };
```

Next up we define two instances of `Validator<string>`: `slugValidator` and `titleValidator`. These objects use the `Validator` type we defined later to determine their structure based on the type argument provided.

The `slugValidator` object specifies some validation rules for a string field.

The `titleValidator` object, on the other hand, focuses on validating a string field for a title. It requires a minimum length of 1, a maximum length of 64, and doesn&rsquo;t have any pattern-based constraints.

Lastly, we encounter an age validator object – `ageValidator`. It&rsquo;s specifically designed to validate numeric values, representing the age of an individual for purchasing alcohol in a sane country (we like sanity!). This validator sets a minimum age of 18 and a maximum age of 149, making sure we don&rsquo;t encounter any age-related shenanigans.

```typescript
    type Field = number | string | unknown;
    type Validator<T extends Field> = T extends number
      ? NumberValidator
      : (T extends string ? StringValidator : NoopValidator);
```

To wrap up the initial example we define a type alias called `Field` that can be either a number, a string, or an unknown type. Then, we define the `Validator` type, which takes a generic argument `T` extending `Field`.

Here&rsquo;s where the conditional magic comes into play: using the `extends` keyword, we check if `T` extends `number`. If it does, the type becomes `NumberValidator`. If `T` extends `string`, the type becomes `StringValidator`. And if none of the conditions match, we fall back to `NoopValidator`. Notice the familiar ternary operator syntax for conditional values in this type expression.

In essence, these mechanics allow us to dynamically determine the structure and properties of the validator object based on the type we provide. It&rsquo;s like having different validator configurations tailored to the type of data we&rsquo;re validating – an incredibly powerful technique!


<a id="orgf8fb829"></a>

## When to use Conditional Types

Picture this: you&rsquo;re tasked with updating different types of objects, but here&rsquo;s the catch—you don&rsquo;t know which type or fields of the type. This exactly where conditional types come to the rescue, swooping in like a superhero to save the day.

Yes, perhaps this is a little contrived to show the power of conditional types mixed with `keyof` for a more involved example I covered [how to use keyof in TypeScript](https://www.susanpotter.net/software/typescripts-keyof-operator-and-possible-uses/) in a previous post.

Now, let me take you on a journey through the magical realm of TypeScript, where we&rsquo;ll witness firsthand how conditional types can boost your code&rsquo;s type safety and eliminate those dreadful runtime bugs for a situation like this.

First things first, what are conditional types? Think of them as intelligent type definitions that adapt based on conditions. They allow us to model complex scenarios, such as handling updates to objects with unknown fields, with elegance and precision.

Imagine you have a function called `updateObject` that takes an original object and an updates object. Traditionally, without conditional types, you might write something like this:
```typescript
    function updateObject(original: any, updates: any): any {
      return { ...original, ...updates };
    }
```
Sure, it gets the job done, but it&rsquo;s like playing with fire. It&rsquo;s untyped, leaving you vulnerable to runtime bugs that may lurk in the shadows. Spelling mistakes, non-existent fields—these errors can easily slip through the cracks, only to haunt you later.

Also, if you have read my previous post on [the differences between `any` versus `unknown` in TypeScript](https://www.susanpotter.net/software/any-versus-unknown-in-typescript-when-to-use-one-over-the-other/) you will know `any` typing should only be done as a last resort in TypeScript.

Fear not, reader! Conditional types are here to save us from this chaos. Let&rsquo;s dive into the code and see how we can level up our type safety game.

We&rsquo;ll start by defining a conditional type called `UpdateObject`. Brace yourself for the magic:
```typescript
    type UpdateObject<T, U> = {
      [K in keyof T]: K extends keyof U ? U[K] : T[K];
    };
```
Let&rsquo;s break the above down. The `UpdateObject` type takes two generic parameters, `T` and `U`, representing the original object and the updates object, respectively. Now, pay close attention to the square brackets `[K in keyof T]`.

This snippet introduces a loop that iterates over each property `K` in `T`, which is the key type of the original object. But here comes the real beauty—the conditional statement: `K extends keyof U ? U[K] : T[K]`.

This line of code works its magic by checking if `K` exists in the keys of `U`, our updates object. If it does, TypeScript gracefully assigns the type of `U[K]` to the property in the resulting object. However, if the property doesn&rsquo;t exist in `U`, it falls back to the type of `T[K]`, ensuring we maintain the original value.

Now that we have our powerful `UpdateObject` type, let&rsquo;s put it into action by revamping our `updateObject` function:
```typescript
    function updateObject<
        T,
        U extends Partial<T>
      >(original: T, updates: U): T {
        return { ...original, ...updates };
    }
```

Bear with me, folks—we&rsquo;re almost there! The `updateObject` function now receives two parameters: `original`, representing the original object, and `updates`, holding our updates object. But what&rsquo;s this `<T, U extends Partial<T>>` magic?

Here, we&rsquo;re using TypeScript&rsquo;s generic parameters to ensure type safety. `T` represents the original object&rsquo;s type, while `U` extends `Partial<T>`. This ensures that the `updates` object can only contain a subset of properties from the original object, avoiding unexpected additions.

With our revamped function in place, we can now unleash the power of TypeScript&rsquo;s type system to catch bugs at compile-time, rather than chasing them through the treacherous land of runtime.

Let&rsquo;s say we have a `Person` type:
```typescript
    type Person = {
      name: string;
      age: number;
      address: string;
    };
```
Using our updated function, we can confidently update a `Person` object, knowing that TypeScript will guard us against any mishaps:
```typescript
    const person: Person = {
      name: 'Alice',
      age: 25,
      address: '123 Main St'
    };
    
    const updatedPerson = updateObject(person, {
      age: 26,
      address: '456 Elm St'
    });
```

Trying to pass a partial object for the `updates` with a misspelled attribute will now result in a type error. Like the following:
```typescript
    const updatedPerson2: Person =
      updateObject(person, { adress: 232, });
```
This will produce the type error:
```typescript
    Argument of type '{ adress: number; }' is not assignable
      to parameter of type 'Partial<Person>'.
      Object literal may only specify known properties, but
      'adress' does not exist in type 'Partial<Person>'.
      Did you mean to write 'address'?
```

Boom! With TypeScript&rsquo;s watchful eye, any misspelled field names or attempts to update non-existent fields will be caught right at compile-time. No more runtime surprises, my friends!


<a id="org107a656"></a>

## But wait, we didn&rsquo;t use `UpdateObject<T, U>` anywhere

Good eye, reader. This was pointed out to me after I published a draft for people to review. I realized in the second case the `Partial<T>` type was all the magic this case needed. No need to get fancy with conditional types.

So, dear developers, embrace the power of TypeScript and awe inspiring types like `Partial<T>`. Say goodbye to the unpredictable world of runtime bugs, and stride confidently towards a future of reliable and type-safe code.


<a id="org657ff19"></a>

## Embrace the Power of Conditional Types but only when you really need to! :)

Well the moral of this story is that next time I will not sit on a half finished blog post for so long such that I lose all my train of thought on the more &ldquo;motivating&rdquo; example to use such that it is essentially an invalid example for what I wished to demonstrate.

Forgive me, reader and take care until next time where we explore [mapped types](https://www.susanpotter.net/software/conjuring-typescripts-magic-with-mapped-types/) and even combine it with **conditional types** that we just explored here.