loading...

Notes on TypeScript: Type Level Programming Part 1

busypeoples profile image A. Sharif Updated on ・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.

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

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

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

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

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

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

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

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

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

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

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

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

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 by:

busypeoples profile

A. Sharif

@busypeoples

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

Discussion

markdown guide
 

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?

 

Thanks Cezar!

Updated the post.

 

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