• #typescript
  • #types
  • #beginners

TypeScript Utility Types You Should Actually Use.

A practical, in-depth guide to Partial, Pick, Omit, and Record, the utility types that show up in real codebases constantly, and how they save you from rewriting the same interface five different ways.

Kituu · 7 min read ·

The Problem These Solve

You've got an interface. It's used all over your app.

interface User {
	id: number;
	name: string;
	email: string;
	role: 'admin' | 'member';
}

Then one day you need an "update user" function, and it turns out not every field is required for an update. Maybe someone's just changing their name. The naive move is to write a second, almost identical interface:

interface UpdateUserInput {
	id?: number;
	name?: string;
	email?: string;
	role?: 'admin' | 'member';
}

It works, but now you have two interfaces to keep in sync. Add a field to User next month, forget to add it to UpdateUserInput, and you've got a quiet bug waiting to happen.

Utility types exist so you never have to write that second interface by hand. They take a type you already have and reshape it, so the two stay connected automatically. Change User, and everything derived from it updates on its own.


Partial<T> Every Field, Now Optional

Partial takes a type and makes every property optional. That "update user" problem from above:

interface User {
	id: number;
	name: string;
	email: string;
	role: 'admin' | 'member';
}

function updateUser(id: number, changes: Partial<User>) {
	// changes might have any subset of User's fields
}

updateUser(1, { name: 'Amara' }); // fine
updateUser(1, { name: 'Amara', role: 'admin' }); // also fine
updateUser(1, {}); // also fine, technically

One line instead of a whole second interface. And if you add a phone field to User tomorrow, Partial<User> automatically includes it as an optional field, no extra work.

This is the one you'll reach for constantly: form state before submission, PATCH request bodies, anywhere you're building something up piece by piece instead of getting it all at once.

function createDraft(defaults: Partial<User> = {}) {
	return {
		id: 0,
		name: '',
		email: '',
		role: 'member',
		...defaults
	};
}

Pick<T, K> Only The Fields You Actually Want

Pick takes a type and a list of keys, and gives you back a new type with only those keys.

interface User {
	id: number;
	name: string;
	email: string;
	role: 'admin' | 'member';
}

type UserPreview = Pick<User, 'id' | 'name'>;

const preview: UserPreview = {
	id: 1,
	name: 'Amara'
};

UserPreview only has id and name. Trying to add email to it would be a type error, and leaving out id or name would be too. It's exactly the shape you asked for, nothing more, nothing less.

This shows up constantly when a component or function only needs a slice of a bigger object. A user list item doesn't need the full User, it needs a name and maybe an avatar:

type UserListItem = Pick<User, 'id' | 'name'>;

function renderListItem(user: UserListItem) {
	console.log(user.name);
}

Now renderListItem can't accidentally depend on email or role, because those fields don't exist on the type it accepts. That's not just convenience, it's a guardrail that keeps a small component from quietly growing a dependency on data it was never meant to need.


Omit<T, K> Everything Except These Fields

Omit is Pick's mirror image. Instead of naming the fields you want, you name the ones you want to exclude, and keep everything else.

interface User {
	id: number;
	name: string;
	email: string;
	role: 'admin' | 'member';
}

type NewUserInput = Omit<User, 'id'>;

const newUser: NewUserInput = {
	name: 'Ruguru',
	email: 'ruguru@example.com',
	role: 'member'
};

NewUserInput has everything User has except id, which makes sense, since the database assigns that when the row gets created. You never pass one in.

Omit and Pick solve the same kind of problem from opposite directions. Reach for Pick when the list of fields you want is short. Reach for Omit when it's the list of fields you don't want that's short. Either way, you're describing a shape by reference to a type you already have, instead of writing it out fresh and hoping it stays accurate.

type PublicUser = Omit<User, 'email'>;

function sendToClient(user: User): PublicUser {
	const { email, ...rest } = user;
	return rest;
}

Record<K, V> A Typed Map From Keys To Values

Record builds an object type where every key is of type K and every value is of type V.

const rolePermissions: Record<'admin' | 'member', string[]> = {
	admin: ['read', 'write', 'delete'],
	member: ['read']
};

TypeScript knows exactly two keys are allowed here, admin and member, because that's what the union type says. Try to add a guest key, or forget one of the two, and TypeScript will flag it.

const rolePermissions: Record<'admin' | 'member', string[]> = {
	admin: ['read', 'write', 'delete']
	// Error: Property 'member' is missing
};

That's a genuinely useful check. Without Record, a plain object literal wouldn't complain if you forgot a case, and you'd only find out at runtime when someone with the member role hit undefined behavior.

Record is also the cleaner way to write what you might otherwise reach for an index signature for:

// works, but doesn't say much about what the keys actually are
const scores: { [key: string]: number } = {
	Amara: 95,
	Ruguru: 88
};
// says exactly what's meant: a map of string keys to number values
const scores: Record<string, number> = {
	Amara: 95,
	Ruguru: 88
};

Same result, but Record<string, number> reads like what it is: a map of keys to values.


Putting Them Together

These four aren't just useful alone, they combine well too. A common pattern is narrowing a type with Pick or Omit, then relaxing it with Partial for the cases where every field is optional.

interface User {
	id: number;
	name: string;
	email: string;
	role: 'admin' | 'member';
}

// what you send to create a user: no id, all fields required
type CreateUserInput = Omit<User, 'id'>;

// what you send to update a user: no id, all fields optional
type UpdateUserInput = Partial<Omit<User, 'id'>>;

function createUser(input: CreateUserInput) {
	/* ... */
}

function updateUser(id: number, input: UpdateUserInput) {
	/* ... */
}

Both CreateUserInput and UpdateUserInput are derived from User. Add a field to User, and both update automatically, no separate interfaces to remember to touch.

You can go further and build a lookup table keyed by ID, using Record and Pick together:

type UserSummary = Pick<User, 'id' | 'name'>;

const usersById: Record<number, UserSummary> = {
	1: { id: 1, name: 'Amara' },
	2: { id: 2, name: 'Ruguru' }
};

Bringing It All Together

  1. Partial<T> makes every field on a type optional, ideal for update payloads and objects built up incrementally.
  2. Pick<T, K> keeps only the listed keys, useful when something only needs a small slice of a bigger type.
  3. Omit<T, K> keeps everything except the listed keys, useful when you're excluding one or two fields from an otherwise complete type.
  4. Record<K, V> builds an object type from a set of keys to a value type, and will catch you if you miss a case in a known, finite set of keys.
  5. These combine naturally: narrow with Pick or Omit, then relax with Partial, all derived from one source type instead of duplicated by hand.

Once these four are part of how you think about shaping data, you'll stop writing a fresh interface every time you need a slightly different version of something you already defined. You'll reshape the one you have instead, and let TypeScript keep every version of it honest.