DEV Community

Cover image for 5 Typescript utility types, that will make your life easier
Gleb Irovich
Gleb Irovich

Posted on

5 Typescript utility types, that will make your life easier

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
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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"
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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
}; 
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
nosyminotaur profile image
Harsh Pathak

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!

Collapse
 
glebirovich profile image
Gleb Irovich

React with typescript is awesome, especially once you manage to properly type HOCs 😂