The Utility Types
A dozen built-in transformations that stop you from writing the same shape twice. Partial, Pick, Omit, Record and the rest.
By the end: Derive the type you need from one you already have.
TypeScript ships about a dozen built-in type transformations. They exist so you never write the same shape twice, and knowing them is mostly a matter of recognition rather than understanding — each one does something small and obvious once you have seen it.
Lesson 17 shows how they are implemented. This lesson is about using them, which is the part you need every day.
Partial, Required and Readonly
interface User {
id: number;
name: string;
email?: string;
}
// Every property optional. The classic use is an update payload.
type UserPatch = Partial<User>;
// Every property required, including ones that were optional.
type CompleteUser = Required<User>;
// Every property readonly.
type FrozenUser = Readonly<User>;
const patch: UserPatch = { name: 'Ada' };
const complete: CompleteUser = { id: 1, name: 'Ada', email: 'ada@example.com' };
const frozen: FrozenUser = { id: 2, name: 'Grace' };
function applyPatch(user: User, changes: Partial<User>): User {
return { ...user, ...changes };
}
console.log(applyPatch(complete, patch));
console.log(complete.email.toUpperCase());
console.log(frozen.name);
Output
interface User {
id: number;
name: string;
}
const user: Readonly<User> = { id: 1, name: 'Ada' };
user.name = 'Grace';
console.log(user);
What the compiler says
error TS2540: Cannot assign to 'name' because it is a read-only property.
interface Settings {
theme: string;
layout: { columns: number; gutter: number };
}
// `layout` itself is now optional. Its contents are not.
const patch: Partial<Settings> = { layout: { columns: 3 } };
console.log(patch);
What the compiler says
error TS2741: Property 'gutter' is missing in type '{ columns: number; }' but required in type '{ columns: number; gutter: number; }'.
Pick and Omit
interface User {
id: number;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
}
// Keep only what you name.
type PublicUser = Pick<User, 'id' | 'name'>;
// Keep everything except what you name.
type SafeUser = Omit<User, 'passwordHash'>;
// They compose, which is where the value is.
type UserSummary = Pick<SafeUser, 'id' | 'name' | 'email'>;
const publicUser: PublicUser = { id: 1, name: 'Ada' };
const safe: SafeUser = {
id: 1,
name: 'Ada',
email: 'ada@example.com',
createdAt: new Date('2026-01-01'),
};
const summary: UserSummary = { id: 1, name: 'Ada', email: 'ada@example.com' };
console.log(publicUser);
console.log(safe.email, safe.createdAt.getUTCFullYear());
console.log(summary);
Output
interface User {
id: number;
name: string;
}
type Bad = Pick<User, 'nmae'>;
const value: Bad = {} as Bad;
console.log(value);
What the compiler says
error TS2344: Type '"nmae"' does not satisfy the constraint 'keyof User'.
interface User {
id: number;
name: string;
}
// A typo. No error, and the resulting type still has both properties, so the
// mistake surfaces later as "why is passwordHash still in my response".
type Oops = Omit<User, 'nmae'>;
const value: Oops = { id: 1, name: 'Ada' };
console.log('both properties survived the typo:', Object.keys(value));
// A stricter Omit, constrained to real keys. Worth having in a project.
type StrictOmit<T, K extends keyof T> = Omit<T, K>;
type Safe = StrictOmit<User, 'name'>;
const safe: Safe = { id: 1 };
console.log('strict version:', safe);
Output
Record
// Any string key, values are numbers.
type Counts = Record<string, number>;
// A constrained key set, which is where Record does real work: the compiler
// now insists every member is present.
type Level = 'debug' | 'info' | 'error';
type Colours = Record<Level, string>;
const counts: Counts = { a: 1, b: 2 };
const colours: Colours = { debug: 'grey', info: 'blue', error: 'red' };
console.log(counts['a'] ?? 0);
console.log(colours.error);
// Values can be any type, including functions.
type Handlers = Record<Level, (message: string) => string>;
const handlers: Handlers = {
debug: (m) => `. ${m}`,
info: (m) => `- ${m}`,
error: (m) => `! ${m}`,
};
for (const level of ['debug', 'info', 'error'] as const) {
console.log(handlers[level]('something happened'));
}
Output
type Level = 'debug' | 'info' | 'error';
// Missing `error`. This is the exhaustiveness check from lesson 04, applied to
// an object rather than a switch — and it is why Record over a union is such a
// useful pattern.
const colours: Record<Level, string> = {
debug: 'grey',
info: 'blue',
};
console.log(colours);
What the compiler says
error TS2741: Property 'error' is missing in type '{ debug: string; info: string; }' but required in type 'Record<Level, string>'.
The union transformations
type Level = 'debug' | 'info' | 'warn' | 'error';
// Remove members from a union.
type Quiet = Exclude<Level, 'warn' | 'error'>; // 'debug' | 'info'
// Keep only the members that match.
type Loud = Extract<Level, 'warn' | 'error'>; // 'warn' | 'error'
// Remove null and undefined.
type Maybe = string | null | undefined;
type Definitely = NonNullable<Maybe>; // string
const quiet: Quiet = 'debug';
const loud: Loud = 'error';
const definite: Definitely = 'text';
console.log(quiet, loud, definite);
// Extract is genuinely useful on object unions: pull out one member by its tag.
type Event =
| { kind: 'click'; x: number; y: number }
| { kind: 'key'; code: string }
| { kind: 'scroll'; delta: number };
type KeyEvent = Extract<Event, { kind: 'key' }>;
function describeKey(event: KeyEvent): string {
return `key ${event.code}`;
}
console.log(describeKey({ kind: 'key', code: 'Enter' }));
Output
Function and constructor transformations
function findUser(id: number, includeDeleted = false): { id: number; name: string } | undefined {
return id === 1 ? { id: 1, name: 'Ada' } : undefined;
}
async function fetchTitle(): Promise<string> {
await Promise.resolve();
return 'Document';
}
// The return type, without writing it out.
type FoundUser = ReturnType<typeof findUser>; // { ... } | undefined
// The parameters, as a tuple.
type FindArgs = Parameters<typeof findUser>; // [number, boolean?]
// Unwrap a Promise.
type Title = Awaited<ReturnType<typeof fetchTitle>>; // string
// And these compose, which is the usual reason to reach for them.
type DefiniteUser = NonNullable<ReturnType<typeof findUser>>;
const found: FoundUser = { id: 1, name: 'Ada' };
const args: FindArgs = [1, true];
const definite: DefiniteUser = { id: 1, name: 'Ada' };
console.log(found?.name, args[0], definite.name);
console.log('title type is a plain string:', await fetchTitle());
// Spreading a Parameters tuple back into the call is a neat trick for wrappers.
function logged(...params: FindArgs): FoundUser {
console.log('calling with', params);
return findUser(...params);
}
console.log(logged(1)?.name);
Output
class Connection {
constructor(
public readonly host: string,
public readonly port: number,
) {}
describe(): string {
return `${this.host}:${this.port}`;
}
}
// The type of an instance, from the class value.
type Conn = InstanceType<typeof Connection>;
// The constructor's arguments, as a tuple.
type ConnArgs = ConstructorParameters<typeof Connection>;
const conn: Conn = new Connection('localhost', 5432);
const args: ConnArgs = ['db.internal', 6543];
console.log(conn.describe());
console.log(new Connection(...args).describe());
// This is how you type a factory that takes a class and builds one.
function build<T extends new (...args: never[]) => unknown>(
Ctor: T,
...args: ConstructorParameters<T>
): InstanceType<T> {
return new Ctor(...(args as never[])) as InstanceType<T>;
}
console.log(build(Connection, 'example.org', 443).describe());
Output
The string transformations
type Method = 'get' | 'post' | 'delete';
type Loud = Uppercase<Method>; // 'GET' | 'POST' | 'DELETE'
type Titled = Capitalize<Method>; // 'Get' | 'Post' | 'Delete'
const loud: Loud = 'POST';
const titled: Titled = 'Delete';
console.log(loud, titled);
// They combine with template literal types to derive names mechanically.
type Handler = `handle${Capitalize<Method>}`; // 'handleGet' | 'handlePost' | ...
const handlerName: Handler = 'handlePost';
console.log(handlerName);
// Which lets you demand a handler per method, checked.
type Handlers = { [K in Method as `handle${Capitalize<K>}`]: () => string };
const handlers: Handlers = {
handleGet: () => 'reading',
handlePost: () => 'creating',
handleDelete: () => 'removing',
};
console.log(handlers.handleGet(), handlers.handleDelete());
Output
Putting them together
// The single definition. Everything else follows from it.
interface User {
id: number;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
lastSeenAt?: Date;
}
// What the API returns: no secrets, dates as strings over the wire.
type UserResponse = Omit<User, 'passwordHash' | 'createdAt' | 'lastSeenAt'> & {
createdAt: string;
lastSeenAt?: string;
};
// What creating one requires: no id, no timestamps, plus a plain password.
type CreateUser = Pick<User, 'name' | 'email'> & { password: string };
// What updating one allows: any subset of the creatable fields.
type UpdateUser = Partial<CreateUser>;
// A read-only view for rendering.
type UserView = Readonly<UserResponse>;
function toResponse(user: User): UserResponse {
const base = {
id: user.id,
name: user.name,
email: user.email,
createdAt: user.createdAt.toISOString(),
};
return user.lastSeenAt === undefined
? base
: { ...base, lastSeenAt: user.lastSeenAt.toISOString() };
}
const stored: User = {
id: 1,
name: 'Ada',
email: 'ada@example.com',
passwordHash: 'x'.repeat(60),
createdAt: new Date('2026-01-01T00:00:00Z'),
};
const response: UserResponse = toResponse(stored);
const create: CreateUser = { name: 'Grace', email: 'grace@example.com', password: 'secret' };
const update: UpdateUser = { email: 'new@example.com' };
const view: UserView = response;
console.log(response);
console.log(create.name, Object.keys(update));
console.log('no passwordHash in the response:', !('passwordHash' in response));
console.log('view is the same data, read-only:', view.name);
Output
The full list, briefly
interface Example {
a: string;
b?: number;
readonly c: boolean;
}
type Fn = (x: number, y: string) => Promise<boolean>;
class Cls { constructor(public n: number) {} }
// Over object properties.
type T1 = Partial<Example>;
type T2 = Required<Example>;
type T3 = Readonly<Example>;
type T4 = Pick<Example, 'a' | 'b'>;
type T5 = Omit<Example, 'c'>;
type T6 = Record<'x' | 'y', number>;
// Over unions.
type T7 = Exclude<'a' | 'b' | 'c', 'c'>;
type T8 = Extract<'a' | 'b' | 'c', 'a' | 'z'>;
type T9 = NonNullable<string | null | undefined>;
// Over functions and classes.
type T10 = ReturnType<Fn>;
type T11 = Parameters<Fn>;
type T12 = Awaited<ReturnType<Fn>>;
type T13 = InstanceType<typeof Cls>;
type T14 = ConstructorParameters<typeof Cls>;
type T15 = ThisParameterType<(this: Example, n: number) => void>;
// Over strings.
type T16 = Uppercase<'get'>;
type T17 = Lowercase<'GET'>;
type T18 = Capitalize<'get'>;
type T19 = Uncapitalize<'Get'>;
// One value of each, to prove they resolve to what you would expect.
const values = {
partial: { a: 'x' } satisfies T1,
picked: { a: 'x' } satisfies T4,
omitted: { a: 'x' } satisfies T5,
record: { x: 1, y: 2 } satisfies T6,
excluded: 'a' satisfies T7,
extracted: 'a' satisfies T8,
nonNull: 'x' satisfies T9,
returned: true satisfies T12,
params: [1, 'x'] satisfies T11,
instance: new Cls(1) satisfies T13,
ctorArgs: [2] satisfies T14,
upper: 'GET' satisfies T16,
capital: 'Get' satisfies T18,
};
console.log(values.record, values.excluded, values.upper);
console.log(values.instance.n, values.ctorArgs, values.params);
console.log('every one of these resolved at compile time and vanished');
const t2: T2 = { a: 'x', b: 1, c: true };
const t3: T3 = { a: 'x', c: false };
const t10: T10 = Promise.resolve(true);
const t15: T15 = { a: 'x', c: true };
const t17: T17 = 'get';
const t19: T19 = 'get';
console.log(t2.b, t3.c, await t10, t15.a, t17, t19);
Output
Your turn
- Write a
Productinterface with five fields including a secret one. Derive a public response type withOmitand a creation type withPick. - Write an update function taking
Partial<Product>. Then add a nested object toProductand confirmPartialdoes not make its contents optional. - Make a typo in a
Pickand read the error. Make the same typo in anOmitand note that there is no error at all. - Write
type Levelas a union of four values and aRecord<Level, string>with only three entries. Read the error. Then add a fifth level and watch the same error return. - Use
Extractto name one member of a discriminated union, and write a function taking exactly that member. - Write a function, then derive its parameter tuple with
Parametersand write a wrapper that logs its arguments and forwards them with a spread.
Recap
Partial,RequiredandReadonlytransform every property. All three are shallow — nested objects are untouched.Picknames what to keep and is checked against real keys;Omitnames what to drop and silently accepts a typo. PreferPick, or write a constrainedStrictOmit.Record<Union, V>demands an entry for every union member, which gives you an exhaustiveness check on an object for free.ExcludeandExtractoperate on unions;Extract<Event, { kind: 'key' }>is how you name one member of a discriminated union without repeating it.ReturnType,ParametersandAwaitedderive from functions. Useful for other people's code; a sign of a missing named interface in your own.- Derive rather than duplicate. One interface plus five utility types beats five hand-written interfaces that drift apart.