Luckily for us, developers, Typescript is shipped with a lot of handy utility types. They are meant to improve the readability of the code and reduce the boilerplate while working with types. In today's episode of Typescript 101, I would like to talk about 5 utility types, which I find especially useful in everyday development.
Table of content
1. Omit
Omit<T, K>
according to the official documentation Constructs a type by picking all properties from T and then removing K.
In other words Omit
is a generic utility type, that drops keys of T
specified in K
. One of the use-cases where you might need this utility is working with DTOs. If your project is using a strict serialisation, you might find yourself creating a lot of boilerplate code to describe different DTOs. Let's consider an example of how we can benefit from Omit
in that case:
interface Post {
id: number;
title: string;
createdAt: string;
}
type CreatePostDto = Omit<Post, "id" | "createdAt">;
const createDto: CreatePostDto = {
title: "My post",
id: 1, // error
createdAt: "2020-06-06" // error
};
Properties like id
or createdAt
are usually set by the backend and you don't have them available while creating a new entity via the API. You can simply describe this situation by omitting those keys from the Post
interface.
2. Pick
Pick
does the opposite of Omit
. Pick<T, K>
Constructs a type by picking the set of properties K from T.
Continuing the same example with DTOs, here is how you can define a type of UpdatePostDto
:
type UpdatePostDto = Pick<Post, "id" | "title">;
const updateDto: UpdatePostDto = {
id: 1,
title: "My new post",
createdAt: "2020-06-06" // error
};
Pick
and Omit
can be used to achieve the same goal because Pick<Post, "id" | "title">
is the same as Omit<Post, "createdAt">
. You can always decide what is shorter or more readable to use.
3. Partial
Partial<T>
is a generic utility type, that makes all properties of the provided interface optional. My favourite example of using Partial
is updating objects via merging. It's especially common when you are working with state management and state updates.
interface AppState {
posts: Post[];
userName: string;
}
function updateState(state: AppState, update: Partial<AppState>): AppState {
return { ...state, ...update };
}
const initialState: AppState = {
posts: [],
userName: "Gleb"
};
const update: Partial<AppState> = {
userName: "John"
};
updateState(initialState, update);
Partial
sets all properties of AppState
to optional and therefore allows you to define only updated keys, without losing type safety.
4. Readonly
Readonly<T>
is another handy utility, which helps while working with immutable data. If you'd like to enforce immutability try using Readonly
:
const state: Readonly<AppState> = {
posts: [],
userName: "Gleb"
};
state.posts = []; // error: Cannot assign to 'posts' because it is a read-only property.
const updatedState: Readonly<AppState> = { ...state, posts: [] }; // ok
5. Record
I have already talked about Record<T, K>
in this post, but this utility is definitely worth mentioning one more time.
During my daily duties, I have to deal a lot with data grids. Most of them have a very similar pattern: they define every row as a key-value map. It's often the case that the row interface can be defined quite loosely:
type Cell = string;
interface Row {
[key: string]: Cell;
}
It means that you can potentially add as many keys as you want. Here is an example of the row that represents a single post object:
const post: Post = { id: 1, title: "My post", createdAt: "2020-06-06" };
const row: Row = {
title: post.title,
createdAt: post.createdAt,
randomKey: "is allowed"
};
Luckily there is a nice way to constrain allowed keys by using Record
:
type PostRow = Record<keyof Post, Cell>;
const postRow: PostRow = {
id: post.id.toString(),
title: post.title,
createdAt: post.createdAt,
randomKey: "is not allowed" // error
};
This approach makes the expected type of the row transparent to the developers and keeps it type-safe. Also with just a little effort, you can create a reusable generic row type:
type PostRow<T> = Record<keyof T, Cell>;
const postRow: PostRow<Post> = {
id: post.id.toString(),
title: post.title,
createdAt: post.createdAt,
randomKey: "is not allowed" // error
};
Summary
Today we discovered some superpowers of Typescript utility types and I hope you have enjoyed exploring them with me! I am very interested in your feedback. If you want to learn something specific about Typescript or webdev in general, leave a comment and let's discuss it together.
If you liked my post, please spread a word and follow me on Twitter 🚀 for more exciting content about web development.
Top comments (2)
I've used the first 3 a lot when creating prop interfaces in React. There are a lot of scenarios where you need to extend from a base interface and sometimes you require the properties of the base interface to be optional. And sometimes you need just some of the properties of the base parent. Typescript utility types make this process a lot easier.
Although, I've never used Record. I've seen it in Mui but not used it in my own code. Seems handy, thanks!
React with typescript is awesome, especially once you manage to properly type HOCs 😂