跪拜 Guibai
← Back to the summary

TypeScript's Type Machinery: How keyof, Exclude, and Pick Compose Into Omit

The previous article introduced commonly used utility types in TypeScript:

The problem these utility types solve is:

How to quickly generate new types based on existing types.

But behind them lies an even more important idea:

TypeScript types themselves can be computed and combined.

This article continues learning several more fundamental type tools in TypeScript:

And understand why Omit can be implemented through the combination of other types.


1. keyof: Get all field names of an object type

Suppose there is a User type:

interface User {
    id: number;
    name: string;
    age: number;
    email: string;
}

This type contains:

User

id
name
age
email

If you want to get all field names of User:

type UserKeys = keyof User;

You get:

type UserKeys = "id" | "name" | "age" | "email";

That is to say:

keyof User

will convert the object type into:

A union type composed of all keys.

It can be understood as:

User

{
    id
    name
    age
    email
}


        |
        ↓


"id" | "name" | "age" | "email"

2. Exclude: Remove members from a union type

Exclude is used to handle union types.

For example:

type All = "id" | "name" | "age" | "email";

Now if you don't want email:

type AfterExclude = Exclude<All, "email">;

Result:

"id" | "name" | "age"

It can be understood as:

Original union type:

id
name
age
email


Delete:

email


Remaining:

id
name
age

Note:

Exclude handles:

Type sets

not objects.

For example:

type Keys = "a" | "b" | "c";

type Result = Exclude<Keys, "b">;

Result:

"a" | "c"

3. The implementation principle of Omit

Previously learned:

type UserSafe = Omit<User, "email">;

Meaning:

Delete email from User.

Result:

{
    id:number;
    name:string;
    age:number;
}

So how does TypeScript implement Omit?

In fact:

Omit<T, K>

is equivalent to:

Pick<T, Exclude<keyof T, K>>

It looks complex, but it's very simple when broken down.


Step 1: keyof gets all fields

keyof User

Gets:

"id" | "name" | "age" | "email"

Step 2: Exclude removes unwanted fields

Exclude<
    "id" | "name" | "age" | "email",
    "email"
>

Gets:

"id" | "name" | "age"

Step 3: Pick reselects fields

Pick<User, "id" | "name" | "age">

Gets:

{
    id:number;
    name:string;
    age:number;
}

Therefore:

Omit<User,"email">

Internal logic:

User

 ↓ keyof

"id" | "name" | "age" | "email"

 ↓ Exclude deletes email

"id" | "name" | "age"

 ↓ Pick

New User type

4. Record: Create key-value mapping types

The previous utility types are all:

Modifications based on existing types.

Whereas Record is:

Create a new object type based on rules.

Syntax:

Record<key type, value type>

For example:

type Dict = Record<string, number>;

Means:

Create an object:

So:

const obj: Dict = {
    a:1,
    b:2
}

Correct.

But:

const obj: Dict = {
    a:"hello"
}

Incorrect.

Because value should be number.


Practical application: Error code mapping

HTTP status codes:

Can define:

type ErrorMsgMap = Record<number, string>;

Then:

const errorMessage: ErrorMsgMap = {
    400:"Bad request parameters",
    401:"Please log in again",
    403:"Insufficient permissions",
    404:"Resource not found",
    500:"Internal server error"
};

Get error message:

function getErrMsg(code:number):string {
    return errorMessage[code] ?? "Unknown error";
}

5. ReturnType: Get function return value type

TypeScript can automatically infer function return values:

function fn(){
    return {
        x:1,
        y:2
    };
}

TypeScript knows:

{
    x:number;
    y:number;
}

If you want to directly get this return type:

type FnReturn = ReturnType<typeof fn>;

Break it down:

typeof fn

Get function type:

() => {
    x:number;
    y:number;
}

ReturnType

Get return value:

{
    x:number;
    y:number;
}

6. The relationship between these utility types

They do not exist independently.

Many complex types are produced through composition:

Object type User

      |
      |
    keyof

      ↓

Field union type

      |
      |
   Exclude

      ↓

Delete fields

      |
      |
    Pick

      ↓

New object type

7. Summary of common utility types

Utility Type Purpose Example
keyof Get all keys of an object keyof User
Pick Select fields Pick<User,'id'>
Omit Delete fields Omit<User,'email'>
Exclude Remove members from a union type Exclude<A,B>
Partial Make all fields optional Partial<User>
Required Make all fields required Required<User>
Record Create key-value mapping Record<string,number>
ReturnType Get function return type ReturnType<typeof fn>

Summary

The core of TypeScript advanced types is not memorizing every utility type, but understanding:

Types can also be computed.

Through:

We can avoid redefining interfaces in large projects, allowing types to automatically synchronize with business changes.

This is also the reason why utility types are heavily used in React + TypeScript projects:

One core type, through type composition, generates the data structures needed for different pages, interfaces, and forms.