The Omit Helper Type in TypeScript
In version 3.5, TypeScript added an Omit<T, K> helper type to the lib.es5.d.ts type definition file that ships as part of the TypeScript compiler. The Omit<T, K> type lets us create an object type that omits specific properties from another object type:
type User = {
id: string;
name: string;
email: string;
};
type UserWithoutEmail = Omit<User, "email">;
// This is equivalent to:
type UserWithoutEmail = {
id: string;
name: string;
};
The Omit<T, K> helper type is defined in lib.es5.d.ts like this:
/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
To untangle this type definition and understand how it works, let's see how we could've come up with our own version of the Omit<T, K> helper type.
Defining the Omit<T, K> Helper Type #
Let's start with the same User type we've seen above:
type User = {
id: string;
name: string;
email: string;
};
First, we need to be able to retrieve all keys of the User type. We can use the keyof operator to retrieve a union of string literal types that contains all property keys of this object type:
type UserKeys = keyof User;
// This is equivalent to:
type UserKeys = "id" | "name" | "email";
Next, we need to be able to exclude a specific string literal type from a union of string literal types. In the case of our User type, we want to exclude the type "email" from the union "id" | "name" | "email". We can use the Exclude<T, U> helper type to do that:
type UserKeysWithoutEmail = Exclude<UserKeys, "email">;
// This is equivalent to:
type UserKeysWithoutEmail = Exclude<"id" | "name" | "email", "email">;
// This is equivalent to:
type UserKeysWithoutEmail = "id" | "name";
The Exclude<T, U> type is defined in lib.es5.d.ts like this:
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
It uses a conditional type and the never type. Using the Exclude<T, U> helper type, we're removing those types in our union type "id" | "name" | "email" that are assignable to the "email" type. That is only true for the string literal type "email" itself, so we're left with the union type "id" | "name".
Finally, we need to create an object type that contains only those properties of our User type whose keys are found in the UserKeysWithoutEmail union type. We can use the Pick<T, K> helper type to pick those properties off of our User type:
type UserWithoutEmail = Pick<User, UserKeysWithoutEmail>;
// This is equivalent to:
type UserWithoutEmail = Pick<User, "id" | "name">;
// This is equivalent to:
type UserWithoutEmail = {
id: string;
name: string;
};
Here's how the Pick<T, K> helper type is defined in lib.es5.d.ts:
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
The Pick<T, K> type is a mapped type that uses the keyof operator and an indexed access type T[P] to retrieve the type of the property P in the object type T.
Now, let's summarize all the type operations we've performed using keyof, Exclude<T, U>, and Pick<T, K> in a single type:
type UserWithoutEmail = Pick<User, Exclude<keyof User, "email">>;
Notice that this type is specific to our User type. Let's make this a generic type so we can reuse it in other places:
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
We can now use this type to compute our UserWithoutEmail type:
type UserWithoutEmail = Omit<User, "email">;
Since object keys can only be strings, numbers, or symbols, we can add a generic constraint to the type parameter K of our Omit<T, K> helper type to only allow types string, number, or symbol for keys:
type Omit<T, K extends string | number | symbol> = Pick<T, Exclude<keyof T, K>>;
The generic constraint extends string | number | symbol is a bit verbose. We can replace the string | number | symbol union type with the keyof any type since the two are equivalent:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
And there we go! We've arrived at the exact definition of the Omit<T, K> helper type as it is found in the lib.es5.d.ts type definition file:
/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Unrolling Omit<User, "email"> #
Here's a step-by-step evaluation of the Omit<User, "email"> type. Try to follow every step to understand how TypeScript is computing the final type:
type User = {
id: string;
name: string;
email: string;
};
type UserWithoutEmail = Omit<User, "email">;
// This is equivalent to:
type UserWithoutEmail = Pick<User, Exclude<keyof User, "email">>;
// This is equivalent to:
type UserWithoutEmail = Pick<User, Exclude<"id" | "name" | "email", "email">>;
// This is equivalent to:
type UserWithoutEmail = Pick<
User,
| ("id" extends "email" ? never : "id")
| ("name" extends "email" ? never : "name")
| ("email" extends "email" ? never : "email")
>;
// This is equivalent to:
type UserWithoutEmail = Pick<User, "id" | "name" | never>;
// This is equivalent to:
type UserWithoutEmail = Pick<User, "id" | "name">;
// This is equivalent to:
type UserWithoutEmail = {
[P in "id" | "name"]: User[P];
};
// This is equivalent to:
type UserWithoutEmail = {
id: User["id"];
name: User["name"];
};
// This is equivalent to:
type UserWithoutEmail = {
id: string;
name: string;
};
Et voilà, our final UserWithoutEmail type.
This article and 44 others are part of the TypeScript Evolution series. Have a look!
