DEV Community

A. Sharif
A. Sharif

Posted on • Edited on

Notes on TypeScript: Type Level Programming Part 1

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.

Type Level

We will define a couple of useful types, that we can leverage when working with TypeScript. As this is intended as an introductory into the topic, we will focus on writing our own types and then replace them with the existing implementations in TypeScript. This approach will help us explore advanced features as well as learn how they are implemented along the way. In the next part of this series we will leverage the knowledge gained to build more custom types.

Readonly

There are time where we want to ensure that a value is not overridden. This can be achieved by using readonly, for example we might want to guarantee that a specific object property should only be read only, we can do so in TypeScript:

type User = {
  readonly id: number;
  name: string;
}
Enter fullscreen mode Exit fullscreen mode

But there are times where we want to ensure that a type is immutable, not only specific properties. To solve this we can write our own MakeReadOnly type definition that accepts a type and ensures that that the newly defined type is readonly.

type MakeReadOnly<Type> = {readonly [key in keyof Type ]: Type[key]};
// Test MakeReadOnly
type ReadOnlyUser =  MakeReadOnly<User>;

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

The MakeReadOnly type we defined above is strictly for learning purposes as TypeScript offers the Readonly that we can leverage to achieve the same outcome like in the previous example.

type ReadOnlyUser =  Readonly<User>;
Enter fullscreen mode Exit fullscreen mode

Partial/Required

There are situations where we might be wanting to transform a given object or need to ensure that all properties are required but are expecting an object with optional properties.
We can write our own functionalities to ensure that we can transform a type definition from partial to required and vice versa.
Next, let's write some types.

type MakePartial<Type> = { [key in keyof Type]?: Type[key] };
type MakeRequired<Type> = { [key in keyof Type]-?: Type[key] };

// Test MakePartial and MakeRequired
type BlogPost = {
  id: number;
  title: string;
  description?: string;
}

type PartialBlogPost = MakePartial<BlogPost>;

/*
type PartialBlogPost {
  id?: number | undefined;
  title?: string / undefined;
  description?: string / undefined;
}
*/

type RequiredBlogPost = MakeRequired<BlogPost>;

/*
type RequiredBlogPost {
  id: number;
  title: string;
  description: string;
}
*/
Enter fullscreen mode Exit fullscreen mode

There is one interesting aspect that we should note here, using -? in our MakeRequired ensures that we remove any optionals, we can use + or - to gain control over the type modifier, check this answer for more information.

Again, our above defined MakePartial and MakeRequired can be replaced by TypeScript's own Partial and Required, which will lead to the same results as our previous example.

type PartialBlogPost = Partial<BlogPost>;

/*
type PartialBlogPost {
  id?: number | undefined;
  title?: string / undefined;
  description?: string / undefined;
}
*/

type RequiredBlogPost = Required<BlogPost>;

/*
type RequiredBlogPost {
  id: number;
  title: string;
  description: string;
}
*/
Enter fullscreen mode Exit fullscreen mode

Pick, Exclude and Omit

There are situations where we need to create a type from an existing type and might need to pick or remove some of the defined properties.

Before implement our MakePick type, let's see how we can extract the intersecting keys between two types.

type MakeIntersect<T, U> = T extends U ? T: never;

// Test MakeIntersect
type User = {
  id: number;
  name: string;
  title: string;
}

type Profile = {
  id: number;
  title: string;
  url: string;
}

type UserProfile = MakeIntersect<keyof User, keyof Profile>;

/*
type UserProfile = "id" | "title";
*/
Enter fullscreen mode Exit fullscreen mode

Now that we know how to find the intersecting keys between two provided types, we can pick the keys for a provided type.

type ProfileSelectedKeys = MakeIntersect<
  keyof Profile,
  "id" | "nonExistingKey" | "title"
>;

/*
type ProfileSelectedKeys = "id" | "title";
*/
Enter fullscreen mode Exit fullscreen mode

This also means we can a write a type, that can pick the intersected keys and return a new type definition, now.

type MakePick<Type, Keys extends keyof Type> = { [Key in Keys]: Type[Key] };

// Test MakePick

type NewProfile = MakePick<Profile, "id" | "title">;

/*
type NewProfile = {
  id: number;
  title: string;
}
*/
Enter fullscreen mode Exit fullscreen mode

Sometimes we want to exclude specific properties when overriding an existing type, so our next step is define a MakeExclude type, that returns all the keys that can't be found.

type MakeExclude<T, U> = U extends T ? never: U;

// Test MakeExclude
type NonExistentKeys = MakeExclude<keyof User, keyof Profile>;

/*
type NonExistentKeys = "name";
*/
Enter fullscreen mode Exit fullscreen mode

The property name doesn't exist in Profile, which means our newly defined type is type NonExistentKeys = "name".

We can replace our previously defined MakePick and MakeExclude with Pick and Exclude that come with TypeScript.

// Test MakeExclude
type NonExistentKeys = Exclude<keyof User, keyof Profile>;

/*
type NonExistentKeys = "name";
*/

type NewProfile = Pick<Profile, "id" | "title">;

/*
type NewProfile = {
  id: number;
  title: string;
}
*/
Enter fullscreen mode Exit fullscreen mode

Finally, we might want to omit properties from a type definition. TypeScript currently doesn't offer an Omit type, but we can implement our own using Exclude and Pick. Also, in the very first part of the "Notes on TypeScript" series we implemented our own Omit already.

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

// Test Omit
type NewProfile = Omit<Profile, "title">;
/*
type NewProfile = {
  id: number;
  url: string;
}
*/
Enter fullscreen mode Exit fullscreen mode

We should have a basic understanding of how to leverage type level programming in TypeScript now.

In the next part we will build more advanced types that will help with the daily work when using TypeScript.

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

Top comments (3)

Collapse
 
cezarneaga profile image
Cezar Neaga

nice one as always Ali!

found some typos i think:

// Test MakePick
type NewProfile = MakePick<Profile, "id" | "title">;

/*
type NewUserProfile = { // this should be NewProfile?

and

We can replace our previously defined MakePick and MakeExclude with Pick and Extract that come with TypeScript.

Extract should be Exclude?

Collapse
 
busypeoples profile image
A. Sharif

Thanks Cezar!

Updated the post.

Collapse
 
faiwer profile image
Stepan Zubashev

Hi. Probably I found one mistake or type in the article. "type NonExistentKeys" will be "url" instead of "name", because "Profile" doesn't contain field "name", and U is for Profile's keys.

typescriptlang.org /play/#code/C4TwDgpgBAqgzhATlAvFA3lAlgEwFxQB2ArgLYBGSA3EQIakQFzCJaEDmNwWwANo1GasONAL5UAUKEhQACogD2AMyz9UGbPiJlKiLj35MWbTlGKJeR4afFTw0ALK0A1hACiADwDGvYjggAPAAqADSwAHzqMFAQHsAQhDhwUEFQAPxEEABuSFAEMJLS0AByCoTqTq6ePn6BriDKsAiIYfWN8sqqEOFAA