loading...

Notes on TypeScript: Conditional Types

busypeoples profile image A. Sharif ・4 min read

Notes on TypeScript (17 Part Series)

1) Notes on TypeScript: Pick, Exclude and Higher Order Components 2) Notes on TypeScript: Render Props 3 ... 15 3) Notes on TypeScript: Accessing Non Exported Component Prop Types 4) Notes on TypeScript: ReturnType 5) Notes on TypeScript: Phantom Types 6) Notes on TypeScript: Type Level Programming Part 1 7) Notes on TypeScript: Conditional Types 8) Notes on TypeScript: Mapped Types and Lookup Types 9) Notes on TypeScript: React and Generics 10) Notes on TypeScript: Fundamentals For Getting Started 11) Notes on TypeScript: Type Level Programming Part 2 12) Notes on TypeScript: Inferring React PropTypes 13) Notes on TypeScript: React Hooks 14) Notes on TypeScript: Recursive Type Aliases and Immutability 15) Notes on TypeScript: Handling Side-Effects 16) Notes on TypeScript: Type Level Programming Part 3 17) Notes on TypeScript: Building a validation library

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"
*/

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

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;

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;

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;
*/

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;
*/

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;
*/

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;
};
*/

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[];
*/

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[];
*/

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;
*/

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

Notes on TypeScript (17 Part Series)

1) Notes on TypeScript: Pick, Exclude and Higher Order Components 2) Notes on TypeScript: Render Props 3 ... 15 3) Notes on TypeScript: Accessing Non Exported Component Prop Types 4) Notes on TypeScript: ReturnType 5) Notes on TypeScript: Phantom Types 6) Notes on TypeScript: Type Level Programming Part 1 7) Notes on TypeScript: Conditional Types 8) Notes on TypeScript: Mapped Types and Lookup Types 9) Notes on TypeScript: React and Generics 10) Notes on TypeScript: Fundamentals For Getting Started 11) Notes on TypeScript: Type Level Programming Part 2 12) Notes on TypeScript: Inferring React PropTypes 13) Notes on TypeScript: React Hooks 14) Notes on TypeScript: Recursive Type Aliases and Immutability 15) Notes on TypeScript: Handling Side-Effects 16) Notes on TypeScript: Type Level Programming Part 3 17) Notes on TypeScript: Building a validation library

Posted on Feb 24 '19 by:

busypeoples profile

A. Sharif

@busypeoples

Focusing on quality. Software Development. Product Management. https://twitter.com/sharifsbeat

Discussion

markdown guide
 

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 🤔

 

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

 

This is so helpful, thank you!

 

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