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:

TYPESCRIPT
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:

TYPESCRIPT
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:

TYPESCRIPT
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:

TYPESCRIPT
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:

TYPESCRIPT
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. Pick uses key constraints to map a filtered subset of properties from a source object. Exclude leverages distributive conditional types, distributing a union over a condition and substituting matching entries with never. Omit combines these operations by picking the exclusion of keys from the key union of the object. Utility types like ReturnType and Parameters use the conditional infer keyword 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 T ensure 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 infer keyword lets you capture and name anonymous types (like return values or function parameters).
  • Omission Strategy: Omit is constructed by picking the keys that remain after excluding the target keys from keyof T.
  • Union Simplification: Evaluating a union member to never removes it entirely from the resulting union type.