Skip to content
C Codeloom
TypeScript

TypeScript Utility Types: Partial, Pick, Omit, and More

A practical guide to TypeScript's built-in utility types — Partial, Required, Readonly, Pick, Omit, Record, ReturnType, and Awaited — with real-world patterns for APIs and forms.

·12 min read · By Yash Kesharwani
Intermediate 13 min read

What you'll learn

  • How Partial, Required, and Readonly transform a shape
  • How Pick and Omit slice an existing type instead of duplicating it
  • How Record builds dictionary-shaped types from keys and values
  • How ReturnType and Awaited derive types from functions and promises
  • Real-world patterns for API payloads, form state, and update endpoints

Prerequisites

TypeScript’s utility types are the everyday tools that keep your type layer DRY. Instead of writing a UserUpdate interface that mirrors User but with every field optional, you write Partial<User>. Instead of a PublicUser interface that omits password, you write Omit<User, "password">. This post covers the seven utility types you will use most, with the real patterns each one solves.

The mental model

Each utility type is a generic that takes a type and returns a transformed version of it. They are implemented in the standard library with TypeScript’s mapped and conditional type machinery — but you do not need to know how they are built to use them. They are pure types: zero runtime cost, no emitted code.

We will start with the simplest and build up.

Partial<T>: every property optional

Partial<T> returns the same shape as T but with every property optional.

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

type UserPatch = Partial<User>;
// equivalent to:
// {
//   id?: number;
//   name?: string;
//   email?: string;
//   age?: number;
// }

The most common place this appears is a PATCH endpoint or an update function — the caller sends only the fields that should change.

function updateUser(id: number, patch: Partial<User>): Promise<User> {
  return fetch(`/api/users/${id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(patch),
  }).then((r) => r.json());
}

updateUser(1, { email: "new@example.com" }); // ok
updateUser(1, {});                            // ok — but useless
updateUser(1, { name: "Ada", role: "admin" }); // Error: 'role' does not exist on Partial<User>.

Partial is shallow. Nested objects do not become partial recursively — only the top-level properties do. If you need deep partials, there are community types like DeepPartial that handle the recursive case; the standard Partial is one level only.

Required<T>: every property mandatory

The opposite of Partial. Every optional property becomes required.

interface FormDraft {
  title?: string;
  body?: string;
  category?: string;
}

type ReadyToPublish = Required<FormDraft>;
// {
//   title: string;
//   body: string;
//   category: string;
// }

function publish(post: ReadyToPublish) {
  // here you know every field is present
}

This is useful when a value goes through stages — optional during editing, required at the moment of save. Required<T> makes the contract visible in the function signature without rewriting the interface.

Readonly<T>: lock the shape

Readonly<T> marks every property as readonly.

interface Config {
  apiUrl: string;
  timeoutMs: number;
}

const config: Readonly<Config> = {
  apiUrl: "/api",
  timeoutMs: 5000,
};

config.timeoutMs = 1000;
// Error: Cannot assign to 'timeoutMs' because it is a read-only property.

Like readonly on a single field, this is a compile-time check. The emitted JavaScript still allows mutation. The point is the contract — function signatures that say “I will not mutate this.” Use Readonly on parameters you receive from outside your module to make that promise explicit.

Pick<T, K>: choose properties by name

Pick<T, K> returns a new type containing only the properties you list.

interface User {
  id: number;
  name: string;
  email: string;
  passwordHash: string;
  createdAt: Date;
}

type PublicUser = Pick<User, "id" | "name" | "email">;
// {
//   id: number;
//   name: string;
//   email: string;
// }

The second argument is a union of literal strings — the property names you want to keep. The compiler verifies each name exists on the source type; misspell one and you get an error rather than a silent miss.

Pick is perfect when a downstream consumer needs a strict subset of a domain object:

  • the columns you select for a table view
  • the fields you serialise into a JWT
  • the shape you return from a “public profile” endpoint
async function getPublicProfile(id: number): Promise<PublicUser> {
  const u = await fetchUser(id);
  return { id: u.id, name: u.name, email: u.email };
}

Omit<T, K>: drop properties by name

Omit<T, K> is the inverse — return everything except the listed properties.

type SafeUser = Omit<User, "passwordHash">;
// {
//   id: number;
//   name: string;
//   email: string;
//   createdAt: Date;
// }

Reach for Omit when the things to remove are easier to list than the things to keep. A large entity with one or two sensitive fields is the textbook case.

Pick and Omit compose well with each other and with Partial:

// Fields a user is allowed to change about themselves
type EditableUser = Partial<Pick<User, "name" | "email">>;
// {
//   name?: string;
//   email?: string;
// }

Reading composed utility types is a skill: parse them inside out. Pick<User, "name" | "email"> gives { name; email }. Then Partial<...> makes both fields optional.

Try it yourself. Pick any interface in a project you already have. Build three derived types from it: a Public version with Omit, a CreateInput version with Omit<T, "id" | "createdAt">, and an UpdateInput version with Partial<CreateInput>. Hover each one to confirm the shapes.

Record<K, V>: dictionary types

Record<K, V> builds an object type whose keys are K and whose values are V.

type Role = "admin" | "editor" | "viewer";

const labels: Record<Role, string> = {
  admin: "Administrator",
  editor: "Editor",
  viewer: "Viewer",
};

console.log(labels.admin); // "Administrator"

Two things to notice.

  • Because Role is a finite union of literal strings, every key must be present. Leave one out and the compiler complains. This is excellent for keeping translation tables, configuration maps, and component lookups complete.
  • When K is string or number, Record accepts arbitrary keys, more like a dictionary:
const scores: Record<string, number> = {};
scores.alice = 91;
scores.bob = 73; // ok

Record<string, V> is roughly equivalent to an index signature { [key: string]: V }. They are interchangeable in most contexts. Record is shorter and reads as intent.

A combined example — a component map from the conditional-rendering article, properly typed:

type IconKind = "info" | "warn" | "error";
const ICONS: Record<IconKind, React.ComponentType> = {
  info: InfoIcon,
  warn: WarnIcon,
  error: ErrorIcon,
};

Forget one of the three icons and the compiler catches it immediately.

ReturnType<T>: read a function’s result type

ReturnType<T> extracts the return type of a function type T.

function getUser(id: number) {
  return { id, name: "Ada", role: "admin" as const };
}

type User = ReturnType<typeof getUser>;
// {
//   id: number;
//   name: string;
//   role: "admin";
// }

The typeof getUser part turns a value into its type — the function signature (id: number) => { id: number; name: string; role: "admin" }. ReturnType<...> then plucks out the return.

Where this shines: a single source of truth for shapes that originate at a function boundary. You write the function, and the type follows automatically. Change the function, and every dependent type updates.

async function loadDashboard(userId: number) {
  return {
    user: await getUser(userId),
    notifications: await getNotifications(userId),
    flags: await getFlags(userId),
  };
}

type Dashboard = Awaited<ReturnType<typeof loadDashboard>>;

Awaited<...> unwraps the promise — more on that next.

Awaited<T>: unwrap a promise

Awaited<T> returns the type a promise resolves to. It recurses through nested promises, which Promise.resolve of a promise also does.

type A = Awaited<Promise<number>>;             // number
type B = Awaited<Promise<Promise<string>>>;    // string
type C = Awaited<number>;                       // number

The most common pairing is Awaited<ReturnType<typeof fn>> for an async function:

async function fetchPosts() {
  const res = await fetch("/api/posts");
  return res.json() as Promise<{ id: number; title: string }[]>;
}

type Posts = Awaited<ReturnType<typeof fetchPosts>>;
// { id: number; title: string }[]

Now the rest of your code can refer to Posts without re-declaring the shape. If fetchPosts changes — adds a field, drops one — every consumer’s types update automatically and the compiler tells you where to react.

A real-world combination: API and form state

The patterns above combine into the type architecture most apps end up with. A single canonical entity, then derived types for different jobs.

// 1. The canonical domain type.
interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  passwordHash: string;
  createdAt: Date;
}

// 2. Shape returned to API consumers — no secrets.
type ApiUser = Omit<User, "passwordHash">;

// 3. Payload to create a user — server generates id, createdAt.
type CreateUserInput = Omit<User, "id" | "createdAt">;

// 4. Payload to update a user — every field optional, password not allowed here.
type UpdateUserInput = Partial<Omit<User, "id" | "createdAt" | "passwordHash">>;

// 5. Form state during create — same shape, but every field starts as a string.
type CreateUserForm = Record<keyof CreateUserInput, string>;

// 6. Map of label strings by role for UI display.
type RoleLabels = Record<User["role"], string>;

What this buys you:

  • One change updates many derived types. Add lastLoginAt to User and every related type adopts it automatically.
  • Removed fields are caught. Drop email from User and every consumer that referenced it via ApiUser, CreateUserInput, etc., stops compiling — you see exactly where to update.
  • No duplication. You never write a parallel IUser and IApiUser that drift apart over months.

User["role"] in RoleLabels is indexed access. Given a type and a key, T[K] is the type at that key. It is a small extra tool that pairs naturally with utility types when you want to pluck out a single property.

Try it yourself. In a project, replace one duplicated interface — say, User and CreateUserRequest — with a canonical User plus CreateUserRequest = Omit<User, "id">. Add a new field to User and watch the form, the API client, and the validators all update with one edit. This is the moment utility types pay back the small learning cost.

Parameters<T> and ConstructorParameters<T> (bonus)

Two more that show up occasionally, included for completeness.

Parameters<T> extracts a tuple of a function’s parameter types:

function send(to: string, subject: string, body: string) {}

type SendArgs = Parameters<typeof send>;
// [to: string, subject: string, body: string]

Useful when you want to wrap a function and forward arguments:

function logged<A extends unknown[], R>(fn: (...args: A) => R) {
  return (...args: A): R => {
    console.log("calling with", args);
    return fn(...args);
  };
}

const loggedSend = logged(send);
loggedSend("a@b.c", "hi", "hello");

ConstructorParameters<T> is the same idea for class constructors. You will reach for it rarely, but when you do, nothing else fits.

A few rules of thumb

After you have used these enough, a few principles emerge.

  1. Define one canonical type per domain concept, then derive the others. Resist the temptation to write parallel Create…, Update…, Public… interfaces by hand.
  2. Compose utility types freely. Partial<Pick<User, "name" | "email">> is fine. Read inside-out. Name the result with a type alias when it appears more than once.
  3. Prefer Record<K, V> over an index signature when K is a known union — it gives you exhaustiveness.
  4. Use ReturnType and Awaited for boundary-driven types. If a function defines the shape, let the type follow.
  5. Keep Readonly on parameters that should not be mutated. It signals intent and catches accidents.

A small worked example

A tiny CRUD layer for a Post resource that uses every utility from this post.

interface Post {
  id: number;
  title: string;
  body: string;
  authorId: number;
  tags: string[];
  draft: boolean;
  createdAt: Date;
  updatedAt: Date;
}

type CreatePostInput = Omit<Post, "id" | "createdAt" | "updatedAt">;
type UpdatePostInput = Partial<Omit<Post, "id" | "createdAt" | "updatedAt">>;
type PostSummary = Pick<Post, "id" | "title" | "authorId" | "draft">;
type PostsByAuthor = Record<number, PostSummary[]>;

async function fetchPost(id: number): Promise<Post> {
  const res = await fetch(`/api/posts/${id}`);
  return res.json();
}

type FetchedPost = Awaited<ReturnType<typeof fetchPost>>;
// Post

function summarise(p: Readonly<Post>): PostSummary {
  return { id: p.id, title: p.title, authorId: p.authorId, draft: p.draft };
}

async function createPost(input: CreatePostInput): Promise<Post> {
  const res = await fetch("/api/posts", {
    method: "POST",
    body: JSON.stringify(input),
  });
  return res.json();
}

async function updatePost(id: number, patch: UpdatePostInput): Promise<Post> {
  const res = await fetch(`/api/posts/${id}`, {
    method: "PATCH",
    body: JSON.stringify(patch),
  });
  return res.json();
}

Six derived types, one source of truth. Add a field to Post and every signature updates.

Recap

You now know:

  • Partial<T> and Required<T> flip optionality across every property
  • Readonly<T> locks a shape against mutation at compile time
  • Pick<T, K> keeps only listed properties; Omit<T, K> drops them
  • Record<K, V> builds a dictionary type, exhaustive when K is a finite union
  • ReturnType<typeof fn> and Awaited<T> derive types from functions and promises
  • Compose utility types to keep one canonical interface and many derived shapes
  • These are zero-cost: they exist only in the type system and never appear in your bundle

Next steps

You now have the practical type-system toolkit most production codebases lean on. The natural follow-up is putting these to work in a real React app — typing props, hooks, events, and forms. Pair the React series with the TypeScript fundamentals, and you have everything you need to ship a strongly-typed, well-structured single-page application.

→ Review: React Components and JSX

Questions or feedback? Email codeloomdevv@gmail.com.