🎉 I'm releasing 12 products in 12 months! If you love product, checkout my new blog workingoutloud.dev

Back to home

Mastering Conditional Types In Typescript

This post will is 2 of 20 for my series on intermediate-to-advance TypeScript tips.

All tips can be run on the TypeScript Playground.

Introduction

Today we are delving into one of the most powerful and flexible aspects of TypeScript's type system - Conditional Types. These types can be a bit tricky to grasp initially, but once understood, they open a plethora of possibilities that significantly streamline your TypeScript code.

What are Conditional Types?

Conditional types help us express non-uniform type mappings, i.e., types that depend on a condition. Their syntax resembles ternary operations (condition ? trueCase : falseCase) in JavaScript, and their primary purpose is to dynamically select types based on other types.

Here is the general form of conditional types:

T extends U ? X : Y

In the above, if T is assignable to U, the type is X. Otherwise, it's Y. This feature is incredibly powerful when we want our type to change depending on the input type.

A simple demonstration

Let's see conditional types in action. Let's say we have a need for a function that will convert a valid string argument to a number or a number argument to a string. We can achieve this with conditional types.

First, we need to define our conditional type:

type NumberToString<T> = T extends number ? string : number;

Here, our conditional type NumberToString takes one type parameter: T for our element type. If T is a number, the return type will be string. Otherwise, it's number.

Now, we can define our function:

function convertType<T>(value: T): NumberToString<T> { if (typeof value === "number") { return value.toString() as NumberToString<T>; } else { return Number(value) as NumberToString<T>; } }

In this example. T is the type of the argument passed to the function. If it's a number, we return a string. Otherwise, we return a number (ignore the fact that we are not handling edge cases here).

If we were to test this in a TypeScript playground, you will see the results of the conditional type with no type errors:

console.log(convertType(10)); // "10" -> function convertType<number>(value: number): string console.log(convertType("20")); // 20 -> function convertType<number>(value: number): string

As you can see, when we called convertType(10), it conditionally decided that the input was number and return value would be a string.

In contrast, when we called convertType("20"), it conditionally decided that the input was string and return value would be a number.

You can see this in action on the TypeScript playground here.

A more complex example: translations

We can use conditional types to safely type a function that might do something like grabbing translations from a file.

Consider a translations object like this:

const translations = { path: { to: { key: "a string", }, }, };

Our goal is to make sure that when we call translation('path.to.key'), it will work as expected, but calling translation('path.to.invalidKey') and translation('path.to') will result in type errors. Let's see how we can achieve this with conditional types.

First, let's define a utility type that can parse the dot notation path string and convert it into nested keys:

type Path<T, Key extends keyof any = keyof T> = Key extends keyof T ? T[Key] extends object ? T[Key] extends infer R ? `${Key & string}.${Path<R, keyof R>}` | `${Key & string}` : never : `${Key & string}` : never; type Paths<T> = Path<T> extends string ? Path<T> : never;

Now we need to use this Paths type to create our translation function. This function will check if a given path exists within the translations object.

As of the current TypeScript version (up to 5.1.3), there's no straightforward way to infer string literal types from dot-delimited string paths. TypeScript has limitations on handling and inferring nested property paths, especially those provided as string literals. This is why we need to use the Paths type to convert the string path into nested keys. It will return either a valid object or the string. It would work with the keys at each level of the translations object being known at compile time.

function translation<T, P extends Paths<T>>( obj: T, path: P ): P extends keyof T ? T[P] : never { const keys = path.split(".") as Array<keyof T>; let result: any = obj; for (const key of keys) { result = result[key]; } return result; }

In the above code, P extends keyof T ? T[P] : never checks if the path is a key of the translations object. If it is, it returns the translation. If not, it results in a type error.

Here's how you can use this function:

console.log(translation(translations, "path.to")); // { key: 'a string' } console.log(translation(translations, "path.too")); // Error console.log(translation(translations, "path.to.invalidKey")); // Error console.log(translation(translations, "path.to.key")); // 'a string'

If you are 100% certain you only want to return a valid string, then this type of problem can often be more straightforwardly addressed with runtime checks, or with code generation techniques that can produce the necessary string literal types for a specific object structure.

You can see this in action on the TypeScript playground here.

Summary

As we can see, TypeScript's conditional types allow us to create powerful and type-safe functions that can perform complex operations like checking nested keys in objects. This way, we can catch potential issues at compile-time rather than runtime, making our code much more robust and reliable.

In our first example, we introduced conditional types and created a function that could return either a property value from an object or the object itself, depending on the presence of the property. In this post, we leveraged the power of TypeScript to create a translation function that checks the existence of nested keys in an object.

These examples illustrate the flexibility and power of TypeScript's type system, making it an excellent tool for building reliable, maintainable, and error-resistant software. As you continue your TypeScript journey, I encourage you to dive deeper into these powerful features and experiment with them in your projects.

Resources and further reading

Photo credit: joshwithers

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on Visibuild.

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.

Mastering Conditional Types In Typescript

Introduction

Share this post