Rebuilding the built-in utility types is a classic advanced TypeScript exercise, because every one of them is a few lines built on two ideas. Mapped types walk over the keys of a type and transform each property. Conditional types choose between types, distribute over unions and can pattern-match with infer. Once you can write the utilities yourself, their edge cases stop being surprising. Each version below has a My prefix so it doesn’t clash with the global, and for the cases shown it produces the same result as the built-in.
Mapped types and modifiers
A mapped type iterates over a union of keys with [K in ...] and builds a property for each one. T[K] looks up the original property type.
interface User {
id: string;
name: string;
email?: string;
readonly createdAt: Date;
}
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
// Not built in, but a common follow-up question
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type A = MyRequired<User>; // email: string, createdAt still readonly
type B = Mutable<User>; // createdAt writable, email still optionalThe modifiers are ? and readonly. Writing them adds them (+? and +readonly are the explicit spellings of the same thing), and a - prefix removes them. -? also strips the undefined that optionality added, which is why email becomes plain string.
Notice that Mutable kept email optional. When a mapped type iterates over keyof T directly, it’s called homomorphic, and TypeScript preserves each property’s existing modifiers unless you change them. Homomorphic mapped types also map arrays and tuples to arrays and tuples, rather than to objects with numeric keys:
type T1 = MyPartial<[string, number]>; // [string?, number?]
type T2 = MyReadonly<string[]>; // readonly string[]Exclude, Extract and NonNullable
These three work on unions rather than object properties, and all of them rely on distributive conditional types: a conditional on a naked type parameter runs once per union member.
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type Status = "idle" | "loading" | "success" | "error";
type Settled = MyExclude<Status, "idle" | "loading">; // "success" | "error"
type Fns = MyExtract<string | number | (() => void), Function>; // () => void
type MyNonNullable<T> = T & {};
type Name = MyNonNullable<string | null | undefined>; // stringWalk through Settled out loud in an interview: each member is tested separately, "idle" and "loading" become never, and never disappears from a union, leaving "success" | "error". (Distribution is covered in more depth in TypeScript Generics, Explained Through Interview Questions.)
{} means “any value except null and undefined”, so intersecting with it removes the nullish members: null & {} is never, and string & {} is just string. Current TypeScript defines NonNullable this way; older versions used the conditional form T extends null | undefined ? never : T, which gives the same result here.
Pick, Record and Omit
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyRecord<K extends keyof any, V> = { [P in K]: V };
type MyOmit<T, K extends keyof any> = MyPick<T, MyExclude<keyof T, K>>;
type Preview = MyPick<User, "id" | "email">; // { id: string; email?: string }
type Flags = MyRecord<"admin" | "beta", boolean>; // { admin: boolean; beta: boolean }
type NoEmail = MyOmit<User, "email">; // { id; name; readonly createdAt }keyof any is string | number | symbol, every type that can be a property key. Pick keeps modifiers because its keys are constrained to keyof T; Record has no source type, so it has none to keep.
Omit has two well-known quirks. First, its K is keyof any, not keyof T, so a typo like Omit<User, "emial"> compiles and removes nothing. A stricter version uses key remapping (explained below), where mapping a key to never drops it:
type StrictOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
type Oops = StrictOmit<User, "emial">; // ❌ does not satisfy 'keyof User'Second, Omit doesn’t distribute over unions, because keyof (A | B) contains only the keys common to both members.
type Shape =
| { kind: "circle"; radius: number; id: string }
| { kind: "square"; size: number; id: string };
type Broken = Omit<Shape, "id">; // { kind: "circle" | "square" }
type DistributiveOmit<T, K extends keyof any> =
T extends unknown ? Omit<T, K> : never;
type Fixed = DistributiveOmit<Shape, "id">;
// { kind: "circle"; radius: number } | { kind: "square"; size: number }Gotcha
Omiton a discriminated union silently throws away every member-specific property. If you’re building props for a component that wraps a union type, reach for a distributive version.
ReturnType and Parameters
Both use infer to pattern-match a function type and pull out one piece of it.
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
function createUser(name: string, age?: number) {
return { name, age, active: true };
}
type NewUser = MyReturnType<typeof createUser>;
// { name: string; age: number | undefined; active: boolean }
type Args = MyParameters<typeof createUser>; // [name: string, age?: number]
type Bad = MyReturnType<string>; // ❌ does not satisfy the constraintThese operate on types, so you need typeof to get the type of a function value. Parameters come back as a labeled tuple, optional elements included. For an overloaded function, inference uses the last signature: with format(value: string): string declared before format(value: number): number, ReturnType<typeof format> is number.
Interview tip
A good follow-up question is “why
anyin the constraint and notunknown?” Because parameters are contravariant:(x: string) => voidis not assignable to(...args: unknown[]) => unknown, since a function that needs a string can’t accept anything.any(ornever[]) accepts every function.
Awaited, simplified
await unwraps promises recursively, so a promise of a promise of a string gives you a string. A simplified Awaited is a recursive conditional type:
type MyAwaited<T> = T extends PromiseLike<infer V> ? MyAwaited<V> : T;
type A1 = MyAwaited<Promise<Promise<string>>>; // string
type A2 = MyAwaited<number>; // number
type A3 = MyAwaited<Promise<string> | number>; // string | number
async function loadUser() {
return { id: "1" };
}
type Loaded = MyAwaited<ReturnType<typeof loadUser>>; // { id: string }It matches PromiseLike rather than Promise because await works on any thenable, and it distributes over unions like any conditional on a naked type parameter.
Note
The real
Awaitedis more thorough. It accepts any object with a callablethenmethod, infers the value from the first parameter ofthen’s callback, and special-casesnullandundefined. The simplified version is what interviewers expect you to write.
Key remapping and template literals
An as clause in a mapped type lets you compute a new key for each property. Combined with template literal types, that covers most “generate an API from a type” questions.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type PointGetters = Getters<{ x: number; label: string }>;
// { getX: () => number; getLabel: () => string }
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
type StringFields = PickByValue<User, string>; // { id: string; name: string }string & K is there because keyof T can include number and symbol keys, and Capitalize only accepts strings. In StringFields, email is missing because an optional property’s type is string | undefined, which doesn’t extend string.
Template literal types expand unions into every combination, and infer can parse strings:
type Size = "sm" | "lg";
type Tone = "info" | "error";
type ButtonClass = `btn-${Size}-${Tone}`; // 4 combinations
type RouteParams<S extends string> =
S extends `${string}:${infer Param}/${infer Rest}`
? Param | RouteParams<`/${Rest}`>
: S extends `${string}:${infer Param}`
? Param
: never;
type P = RouteParams<"/users/:userId/posts/:postId">; // "userId" | "postId"The interview answer
“All the built-in utilities come from two features. Mapped types like Partial, Required, Readonly, Pick and Record iterate over keys with [K in keyof T] and use ?, readonly and the - prefix to add or remove modifiers; mapping over keyof T preserves existing modifiers. Conditional types handle the rest: Exclude and Extract are distributive conditionals over unions, and ReturnType, Parameters and Awaited use infer to pattern-match a function or promise type.
I’d also mention the sharp edges: Omit accepts keys that don’t exist and collapses discriminated unions, so I sometimes use a stricter or distributive version, and key remapping with as plus template literal types lets me derive things like getter names or route parameters from existing types.”