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
Top comments (3)
nice one as always Ali!
found some typos i think:
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