DEV Community

A. Sharif
A. Sharif

Posted on

Notes on TypeScript: Conditional Types

Introduction

These notes should help in better understanding TypeScript and might be helpful when needing to lookup up how leverage TypeScript in a specific situation. All examples are based on TypeScript 3.2.

Conditional Types

In the last part of the "Notes on TypeScript" series we focused on understanding the basics of type level programming. Before we continue with more advanced type level examples in TypeScript, it might be a good idea to cover another topic first.
In this part of the series we will try to better understand what conditional types are and how we can leverage them when working with TypeScript in an existing application.

In it's most basic form a conditional type is a sort of ternary expression for types. To get a better idea, let's look at a low level example.

type S = string extends any ? "string" : never;

/*
type S = "string"
*/
Enter fullscreen mode Exit fullscreen mode

The release notes for TypeScript 2.8 have a definition for conditional types:

"A conditional type selects one of two possible types based on a condition expressed as a type relationship test"

So if we look at our initial example, we might think about conditional types like the following:

T extends U ? X : Y
Enter fullscreen mode Exit fullscreen mode

This means if type T extends U, then the conditional type will be resolved to X else it will be resolved to Y. This might still sound abstract at first, but we will see the usefulness of this concept in the following examples. Again, we will write some common functions, that will help us display the why and what of conditional types.

type Exclude<T, U> = T extends U ? never : T;
Enter fullscreen mode Exit fullscreen mode

If we look at the above type definition, we can see that we are using a ternary expression to define the return type. never is a low level type, it can not be refined any further and we can leverage the fact that never indicates that this value should never exist, meaning we can filter out a type if it is never.

If we can write an Exclude type, we can also define an Include type.

type Include<T, U> = T extends U ? T : never;
Enter fullscreen mode Exit fullscreen mode

So just by looking at the above definition we can notice that we flipped the returned type. Interestingly Include can be replaced with Extract that is a predefined conditional type in TypeScript, the same goes for our own defined Exclude, which is also predefined.

Now, that we are gaining a better understanding conditional types, let's use what we have learned so far to build a NonNullable conditional type, that excludes any null or undefined types from a provided type.

type NonNullable<T> = Exclude<T, null | undefined>;

// Test NonNullable
type TestNonNullable = NonNullable<string | null | number | undefined>;

/*
type TestNonNullable = string | number;
*/
Enter fullscreen mode Exit fullscreen mode

In the above example we leveraged our previously defined Exclude type, but we can also write it in a more explicit manner.

type NonNullable<T> = T extends null | undefined ? never : T;

// Test NonNullable
type TestNonNullable = NonNullable<string | null | number | undefined>;

/*
type TestNonNullable = string | number;
*/
Enter fullscreen mode Exit fullscreen mode

This is exactly how the predefined NonNullable is implemented in TypeScript. We check if the provided type extends null or undefined and either return a never type or the type itself.

It might be interesting to take a step back and try to better understand what happened when we tested NonNullable type. The following applied when we ran the previous example:

type TestNonNullable =
  | NonNullable<string>
  | NonNullable<null>
  | NonNullable<number>
  | NonNullable<undefined>;

/*
type TestNonNullable = string | number;
*/
Enter fullscreen mode Exit fullscreen mode

We have been implementing existing conditional types so far. To further our understanding of the topic, let's look at an example, where we might want to remove all nullable properties from a type and create a new type that only expects non-nullable types.

type RemoveUndefinable<Type> = {
  [Key in keyof Type]: undefined extends Type[Key] ? never : Key
}[keyof Type];

type RemoveNullableProperties<Type> = {
  [Key in RemoveUndefinable<Type>]: Type[Key]
};

type TestRemoveNullableProperties = RemoveNullableProperties<{
  id: number;
  name: string;
  property?: string;
}>;

/*
type TestRemoveNullableProperties = {
  id: number;
  name: string;
};
*/
Enter fullscreen mode Exit fullscreen mode

The RemoveNullableProperties type removes all optional types as they might be undefined. Mapped and conditional types can be combined to build advanced types as seen in the above example.

Infer and Type Interference

Before we continue implementing more conditional types, let's try to better understand how we can leverage type interference with conditional types. Using the infer keyword, we can tell TypeScript to try to infer the type. Let's take a look at a couple examples, where leveraging infer can help us.

type GetFunctionArgumentTypes<Type> = Type extends (a: infer U) => void
  ? U
  : never;

type TestGetFunctionArgumentTypesA = GetFunctionArgumentTypes<
  (a: number) => void
>;

/*
type GetFunctionArgumentTypes = number;
*/

type GetPropertyTypes<Type> = Type extends {a: infer U, b: infer U} ? U : never;

type TestGetPropertyTypes = GetPropertyTypes<{a: number, b: string[]}>;

/*
type TestGetPropertyTypes = number | string[];
*/
Enter fullscreen mode Exit fullscreen mode

Looking at the above examples, we notice that we can place the infer keyword at the position where we want the type to be inferred. Let's see if we can infer the return type of a function f.e.

type GetReturnType<Type> = Type extends (...a: any[]) => infer R ? R : any;

type TestGetReturnType = GetReturnType<(a: number) => number[]>;

/*
type TestGetReturnType = number[];
*/
Enter fullscreen mode Exit fullscreen mode

Our GetReturnType can infer the return type of a provided function. This is a similar implementation to the TypeScript provided ReturnType. You can read more about ReturnType in one of the previous "Notes on TypeScript" write-ups.

Finally we can extend our previous GetReturnType to infer the instance type of a constructor function.

type GetInstanceType<T> = T extends new (...args: any[]) => infer R ? R : any;

type TestGetInstanceType = GetInstanceType<new (a: number) => number | string | undefined>;

/*
type TestGetReturnType = number | string | undefined;
*/
Enter fullscreen mode Exit fullscreen mode

Again, this implementation is similar to the TypeScript provided conditional type InstanceType.

We should have a good understanding of conditional types and how to leverage them when working with TypeScript at this point. The knowledge gained in this write-up should help us to build more advanced type level programming examples in one of of the upcoming "Notes on TypeScript".

If you have any questions or feedback please leave a comment here or connect via Twitter: A. Sharif

Top comments (4)

Collapse
 
grossbart profile image
Peter Gassner

Thanks for the writeup! There are two spots where I got stuck and I think they are related.

1.

type RemoveUndefinable<Type> = {
  [Key in keyof Type]: undefined extends Type[Key] ? never : Key
}[keyof Type];

2.

type GetPropertyTypes<Type> = Type extends {a: infer U, b: infer U} ? U : never;

In 1. I got confused by the [keyof Type] at the end of the expression; how does it work?

In 2. I got confused by having the same type variable U used twice in different spots returning different values.

There is some form of relationship-forming happening where I can‘t follow because I don‘t know how the compiler works out a solution. Could you expand a bit on this and maybe show step by step how the compiler arrives at a solution? Now that I think of this I believe that understanding this is fundamental to getting how the whole type system works 🤔

Collapse
 
busypeoples profile image
A. Sharif

Thanks for the feedback!
Will try to update the post with further explanations.

Collapse
 
neewbee profile image
stephen

This is so helpful, thank you!

Collapse
 
perjerz profile image
JaMe Siwat Kaolueng

Nice article. Thank you very much. Really want to know about pre-defined type more.