Notes on TypeScript: Type Level Programming Part 3
Tags: typescript, types
Published: 2020-02-11 18:37:37

---
title: Notes on TypeScript: Type Level Programming Part 3
published: true
description: Type Level Programming in TypeScript
tags: typescript, types
series: Notes on TypeScript
---

### Introduction

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

---

### TL;DR:

{% twitter 1227225715332001796 %} 

### Type Level Programming

This is the third part, where we focus on type level programming in TypeScript. For a better understanding of this post, it might be helpful to have a basic understanding of mapped and lookup types as well as conditional types. Additionally reading the first two posts about type level programming can be helpful too.
You can find the following posts here:

- [Notes on TypeScript: Type Level Programming Part 1](https://dev.to/busypeoples/notes-on-typescript-type-level-programming-in-typescript-part-1-i57)

- [Notes on TypeScript: Type Level Programming Part 2](https://dev.to/busypeoples/notes-on-typescript-type-level-programming-part-2-2mof)

- [Notes on TypeScript: Conditional Types](https://dev.to/busypeoples/notes-on-typescript-conditional-types-4bh)

- [Notes on TypeScript: Mapped Types and Lookup Types](https://dev.to/busypeoples/notes-on-typescript-mapped-types-and-lookup-types-i36)

---

### Examples

In this part, we're going to write some helper functions that enables us to do type transformations based on a specified type. There are cases where we might want to make all properties of a specific type `readonly` or `optional` or `required`. There also might be siutations where we need to transform all properties of a defined type to another type.

Let's assume we have `User` type.

```ts
type User = {
  name: string;
  points: number;
  active: boolean;
};
```

How can convert anything of type `boolean` to `true` for example, as there might be a specific part of our application, where a `User` has to be in an active state? There might be situations where we need to transform all properties of a specific type to a different type. One example would be converting a boolean flag to a string after validating some object and wanting to display error messages.

```ts
type User = {
  points: number;
  name: string;
  active: boolean;
};

/**
  type ActiveUser = {
    points: number;
    name: string;
    active: true;
  }
 */
type ActiveUser = ConvertTo<User, boolean, true>;
```

Our `ConvertTo` helper should be able to receive some type `T` and the type we want to convert from as well as the type we want to convert to.

```ts
type ConvertTo<Type, From, To> = {
  [Key in keyof Type]: Required<Type>[Key] extends From ? To : Type[Key];
};
```

The `ConvertTo` helper maps over the provided type and then checks if a property extends the type we want to transform, either keeping the original type or applying the type we want to transform to. One important note here, is that we need to make the lookup strict via `Required<Type>[Key]`, otherwise any optional properties will be skipped in the mapping function.

Next, we will write some helper functions, which we can use a basic building blocks for writing more specific helpers later on. One basic functionality is to be able to filter based on a defined type. The general use case might not be interesting from a user land perspective, as there might not be too many situations where want to remove all properties that are not of a specific type. The `FilterByType` function only returns keys that match the type we want to filter by.

```ts
type FilterByType<T, U> = {
  [Key in keyof Required<T>]: Required<T>[Key] extends U ? Key : never;
}[keyof T];
```

Using the `FilterByType`:

```ts
type User = {
  id: number;
  name: string;
  points: number;
  active: boolean;
};

type FilteredUserKeys = FilterByType<User, number>;
// type FilteredUserKeys = "id" | "points"
```

From the above example we can see that this helper functions filters all the keys that have the type `number` for the provided `User` shape. If we recall, `Omit` and `Pick` both expect a type and the keys to be applied. By combing `Omit` or `Pick` with our previously defined `FilterByType` helper, we can create two new useful helpers: `IncludeByType` and `ExcludeByType`. The next example should help us understand how this works.

```ts
type ExcludeByType<T, U> = Omit<T, FilterByType<T, U>>;

type IncludeByType<T, U> = Pick<T, FilterByType<T, U>>;
```

The two generic helpers `ExcludeByType` and `IncludeByType` expect a type `T` and the type we want to filter by.

```ts
type User = {
  id: number;
  name: string;
  points: number;
  active: boolean;
};

/**
  type UserWithoutTypeNumber = {
    name: string;
    active: boolean;
  }
 */
type UserWithoutTypeNumber = ExcludeByType<User, number>;

/**
  type UserWithOnlyTypeNumber = {
    id: number;
    points: number;
  }
 */
type UserWithOnlyTypeNumber = IncludeByType<User, number>;
```

The above examples are equivalent to writing:

```ts
type UserWithoutTypeNumber = Omit<User, 'name' | 'string'>;

type UserWithOnlyTypeNumber = Pick<User, 'id' | 'number'>;
```

So `IncludeByType` and `ExcludeByType` can be used as further building blocks to construct helpers that take care of transforming types to `readonly`, `optional` or `required`. Now that we have all the low level helper functions in place we can combine these helpers with the TypeScript provided helpers. To make a specified properties read-only by type, we can use `Readonly` and combine it with the type `T` we want to transform.

```ts
type ReadOnlyByType<T, U> = T & Readonly<IncludeByType<T, U>>;
```

The above code could also be rewritten, to make what is happening more clear, like so:

```ts
type ReadOnlyByType<Type, FilterType> = Type & Readonly<IncludeByType<Type, FilterType>>;
```

Using `ReadOnlyByType` would make a defined type `readonly` by filtering all properties that extend the filter-by type, making them `readonly` and then combining them with original structure.

```ts
type User = {
  id: number;
  name: string;
  points: number;
  active: boolean;
};

/**
  type UserNumberReadOnly = {
    readonly id: number;
    name: string;
    readonly points: number;
    active: boolean;
  };
 */
type UserNumberReadOnly = ReadOnlyByType<User, number>;
```

This enables us to prevent `id` and `points` to be overridden in this case.

```ts
const user: UserNumberReadOnly = {
  id: 1,
  name: "test",
  points: 20,
  active: true
};

user.id = 3;
// Cannot assign to 'id' because it is a read-only property.
user.points = 30;
// Cannot assign to 'points' because it is a read-only property.
```

Finally, we might need the capability to transform some type to optional or required for a given shape `T`. Again, by using the TypeScript provided `Partial` we can now provide two helper functions that can either set a specific type to optional and all others to required or vice versa. Using our previously defined `ExcludeByType` or `IncludeByType`, we can make a specified type `U` to optional or required.

```ts
type OptionalByType<T, U> = Partial<IncludeByType<T, U>> & ExcludeByType<T, U>;

type RequiredByType<T, U> = Partial<ExcludeByType<T, U>> & IncludeByType<T, U>;
```

This enables us now to transform our `User` type:

```ts
/**
  type UserNumbersOptional = {
    id?: number;
    name: string;
    points?: number;
    active: boolean;
  };
 */
type UserNumbersOptional = OptionalByType<User, number>;

/**
  type UserOnlyNumbersRequired = {
    id: number;
    name?: string;
    points: number;
    active?: boolean;
  };
 */
type UserNumbersRequired = RequiredByType<User, number>;

```

There might be one last case, where we would like to only convert all properties of defined type to required, but not change the optional/required for any other properties. In this case we can use the TypeScript helper `Required`.

```ts
type OnlyMakeTypeRequired<T, U> = ExcludeByType<T, U> &
  Required<IncludeByType<T, U>>;
```

`OnlyMakeTypeRequired` enables us to only make these properties required that fit a defined type `U`. Our `User` shape might contain optional and required fields, but only the types that match `U` will be transformed to required, leaving all other properties as is. Check the following example for better understanding.

```ts
type User = {
  id: number;
  points?: number;
  name: string;
  active?: boolean;
};

/**
  type User = {
    id: number;
    name: string;
    points: number;
    active?: boolean;
  }
 */
type RequiredByTypeOnlyUser = OnlyMakeTypeRequired<User, number>;

const user: RequiredByTypeOnlyUser = {
  id: 1,
  name: "Test"
};
// Error! points is required now!
// Property 'points' is missing in type '{ id: number; name: string; }' but required in type 'Required<Pick<User, "id" | "points">>'
```

As we have seen in the previous examples, we can build helper functions when needed for specific situations by leverage type level programming. If you're interested to learn more about type level programming, you can study the TypeScript provided helpers like `Partial`, `Pick` or `Readonly`, furthermore the first two posts on type level programming (see links below) in the "Notes on TypeScript" series might be helpful as well.

---

### Links

- [Notes on TypeScript: Type Level Programming Part 1](https://dev.to/busypeoples/notes-on-typescript-type-level-programming-in-typescript-part-1-i57)

- [Notes on TypeScript: Type Level Programming Part 2](https://dev.to/busypeoples/notes-on-typescript-type-level-programming-part-2-2mof)

- [Notes on TypeScript: Conditional Types](https://dev.to/busypeoples/notes-on-typescript-conditional-types-4bh)

- [Notes on TypeScript: Mapped Types and Lookup Types](https://dev.to/busypeoples/notes-on-typescript-mapped-types-and-lookup-types-i36)

- [Code Examples](https://gist.github.com/busypeoples/b2d9ecda58ff25f3206e8c171f04b087)


---

If you have any questions or feedback please leave a comment here or connect via Twitter: [A. Sharif](https://twitter.com/sharifsbeat)