TypeScript: Recreating Built-in Utility Types
A common medium-to-hard TypeScript interview question is:
How do built-in utility types like Pick, Omit, Exclude, ReturnType, and Parameters work under the hood? Write custom generic types to recreate their behaviors from scratch.
Recreating these types demonstrates a clear understanding of generic constraints, mapped types, conditional types, and the infer keyword.
1. Recreating Exclude
Exclude takes a union type T and removes any members assignable to U.
Mechanics
It leverages distributive conditional types. When a generic parameter is a union, conditional types distribute over the union automatically:
type MyExclude<T, U> = T extends U ? never : T;
// Verification
type Status = "success" | "loading" | "error";
type FinalStatus = MyExclude<Status, "loading">;
// Result: "success" | "error"How it works:
"success" extends "loading" ? never : "success"->"success""loading" extends "loading" ? never : "loading"->never"error" extends "loading" ? never : "error"->"error"- The resulting union is
"success" | never | "error", which resolves to"success" | "error".
2. Recreating Pick
Pick constructs a type by picking a subset of properties K from an object type T.
Mechanics
We use generic constraints (extends keyof T) to ensure that K only contains valid keys of T, and then map over K:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Verification
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = MyPick<Todo, "title" | "completed">;
// Result: { title: string; completed: boolean; }3. Recreating Omit
Omit constructs a type by picking all properties from T and then removing K.
Mechanics
We can implement this by combining our Pick and Exclude types:
type MyOmit<T, K extends keyof T> = MyPick<T, MyExclude<keyof T, K>>;
// Verification
type TodoWithoutDesc = MyOmit<Todo, "description">;
// Result: { title: string; completed: boolean; }4. Recreating ReturnType
ReturnType extracts the return type of a function signature.
Mechanics
We use conditional types combined with the infer keyword. The infer keyword lets us declare a type variable inside the condition to capture the return type:
type MyReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
// Verification
const getUser = () => ({ id: 1, name: "Bob" });
type UserResult = MyReturnType<typeof getUser>;
// Result: { id: number, name: string }5. Recreating Parameters
Parameters extracts the argument types of a function as a tuple.
Mechanics
Similar to ReturnType, we use infer but place it on the arguments list instead of the return type:
type MyParameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
// Verification
function saveProfile(name: string, age: number) {}
type ProfileArgs = MyParameters<typeof saveProfile>;
// Result: [name: string, age: number]Senior-Level Interview Answer
TypeScript utility types leverage generic rules to automate structure compilation.
Pickuses key constraints to map a filtered subset of properties from a source object.Excludeleverages distributive conditional types, distributing a union over a condition and substituting matching entries withnever.Omitcombines these operations by picking the exclusion of keys from the key union of the object. Utility types likeReturnTypeandParametersuse the conditionalinferkeyword to dynamically capture types at declaration points (such as function parameters or return values) and extract them at compile time.
Common Interview Mistakes
❌ Omitting generic constraints
For example, writing type MyPick<T, K> = { [P in K]: T[P] } without specifying K extends keyof T. This will cause a compilation error because TypeScript cannot guarantee that K represents valid keys of T, meaning T[P] might be invalid.
❌ Forgetting how never behaves in unions
Thinking that never remains part of the union. In TypeScript, never represents the empty set, so "success" | never automatically simplifies to "success".
Key Takeaways
- Generic Constraints: Constraints like
extends keyof Tensure that type operations only reference valid keys of an object. - Distributive Unions: Union parameters in conditional types distribute automatically, applying the logic to each member.
- Dynamic Capture: The
inferkeyword lets you capture and name anonymous types (like return values or function parameters). - Omission Strategy:
Omitis constructed by picking the keys that remain after excluding the target keys fromkeyof T. - Union Simplification: Evaluating a union member to
neverremoves it entirely from the resulting union type.