Skip to lesson

learningtypescript.org / intermediate / 10-utility-types · lesson 11 of 25

TL;DR

Master TypeScript utility types like Partial, Pick, Omit, and Record. Transform existing types without repetition.

Key concepts

  • TypeScript utility types
  • Partial Pick Omit
  • TypeScript Record type
  • mapped types TypeScript

Utility Types

TypeScript provides built-in utility types that transform existing types. These let you create new types from old ones without repetition.

What You'll Learn

By the end of this lesson you will derive a whole family of types from one Activity entity — a create shape that structurally cannot carry an id, an update shape where every field is optional, and a preview shape holding just the two fields a list row needs — and you will be able to say what each transformation does to the keys underneath, not merely recite its name.

This is the DTO half of the tracker-service milestone. The tracker's service does not hand-maintain a parallel type per operation; it derives them, so that changing the Activity entity changes every derived shape at once. That derivation is what you build here, and the capstone's service methods are typed against exactly these shapes.

You arrive able to read and write a generic type of your own — a name followed by angle brackets, with a type parameter filled at the use site (Generics). Every utility type here is that same machinery, already declared for you in TypeScript's standard library. What is new is what each one does to the type it receives.

Partial and Required

Partial<T> makes all properties optional. Required<T> makes all properties required.

Neither is compiler magic, and knowing the machinery underneath is what lets you predict a utility type instead of memorising it. Each one walks the keys of the type it is given — keyof T, the union of T's property names — and rebuilds the type one key at a time, adding or removing a modifier as it goes. Partial<T> visits every key in keyof T and stamps a ? on it; Required<T> visits the same keys and strips the ? off. Pick<T, K> keeps only the keys you name, Omit<T, K> keeps every key except those, and Record<K, V> builds a fresh type by walking K instead of an existing shape. That is why Required<Settings> below demands both fields rather than just the first one: the rule applies per key, across all of keyof Settings. You will write this key-walking form yourself — it is called a mapped type — after the capstone, in Mapped Types; here you use the six the standard library already ships.

A note on where this sits: the official TypeScript Handbook keeps utility types in its Reference section rather than the core reading flow. This track teaches them here, in the core arc, deliberately — the tracker's service layer needs derived DTOs before the capstone, and deriving them is the practical payoff of the generics you just learned. The machinery that implements them arrives later, in Mapped Types.

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

// Partial — all fields optional (great for updates)
function updateUser(id: number, updates: Partial<User>): User {
  const existing: User = { id, name: "Alice", email: "alice@example.com", age: 30 };
  return { ...existing, ...updates };
}

const updated = updateUser(1, { name: "Alice Smith", age: 31 });
console.log(`Updated: ${updated.name}, age ${updated.age}`);

// Required — all fields required
interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}

function startServer(config: Required<Config>): void {
  console.log(`Server: ${config.host}:${config.port} (debug: ${config.debug})`);
}

startServer({ host: "localhost", port: 3000, debug: false });

For the Predict below, note the exact compiler error a missing required field produces. This fence is marked no-run because its whole point is to fail to compile — the runner strips the types and would happily execute it, which would contradict the lesson:

interface Settings {
  host?: string;
  port?: number;
}

function start(config: Required<Settings>): void {
  console.log(`${config.host}:${config.port}`);
}

start({ host: "localhost" }); // missing port

Under tsc --strict this fails with:

error TS2345: Argument of type '{ host: string; }' is not assignable to parameter of type 'Required<Settings>'.
  Property 'port' is missing in type '{ host: string; }' but required in type 'Required<Settings>'.

The runtime shadow is the reason that error matters: strip the types and the same call runs to completion, printing localhost:undefined. The compiler is the only thing standing between a missing field and an undefined in your output.

Predict

Settings has two OPTIONAL fields. start takes a Required<Settings>, which flips both to mandatory. The call passes only host. One of the two claims below is what the compiler does; which, and what is the first thing you observe? (The playground strips types and would run this either way — it prints localhost:undefined rather than stopping — so reason about the compiler, not the run.)

interface Settings {
host?: string;
port?: number;
}

function start(config: Required<Settings>): void {
console.log(`${config.host}:${config.port}`);
}

start({ host: "localhost" }); // missing port
Continue learning

Pick and Omit

Pick<T, K> creates a type with only selected properties. Omit<T, K> creates a type without specified properties.

The two are interchangeable in principle — anything you can select you could equally exclude — so the choice between them is a maintenance decision, not a correctness one. Pick when the list you want is short and stable, because a new field on the source type will not silently join your derived shape. Omit when the list you want is nearly everything, because then a new field should join automatically and only the excluded ones are worth naming. Read the example below with that question in mind: ArticlePreview names three fields it wants, while a create shape further down omits the one field it must not have.

interface Article {
  id: number;
  title: string;
  body: string;
  author: string;
  publishedAt: string;
  tags: string[];
}

// Pick — only the fields you need
type ArticlePreview = Pick<Article, "id" | "title" | "author">;

const preview: ArticlePreview = {
  id: 1,
  title: "TypeScript Utility Types",
  author: "Alice"
};
console.log(`Preview: "${preview.title}" by ${preview.author}`);

// Omit — everything except specified fields
type CreateArticleInput = Omit<Article, "id" | "publishedAt">;

const input: CreateArticleInput = {
  title: "New Article",
  body: "Article content here...",
  author: "Bob",
  tags: ["typescript", "tutorial"]
};
console.log(`New article: "${input.title}" [${input.tags.join(", ")}]`);

// Combine them.
// TODO: this is close but too permissive — Partial<Article> makes every field
// optional INCLUDING id, so an update can smuggle in a new id. Compose the
// utility that drops a key with the one that makes fields optional, so the
// `sneaky` line below stops compiling. Both utilities are above.
type ArticleUpdate = Partial<Article>;

const update: ArticleUpdate = { title: "Updated Title" };
console.log(`Update: ${JSON.stringify(update)}`);

// Accepted today; should be rejected once you fix ArticleUpdate.
const sneaky: ArticleUpdate = { id: 99 };
console.log(`Sneaky: ${JSON.stringify(sneaky)}`);

Recall

Without scrolling up: every utility type here is written the same way — Partial<User>, Pick<Article, ...>, Omit<Article, ...> — a name followed by angle brackets. In *Generics* you already wrote your own type in exactly that shape (ApiResponse<T>, Container<T>). So what IS a utility type, in the vocabulary *Generics* gave you?

Continue learning

The ArticleUpdate you were asked to fix above composes to Partial<Omit<Article, "id">> — Omit drops the key first, then Partial makes each of the survivors optional. Order matters in the reading, not the result: the type describes an object that may carry any subset of an article's editable fields and no id at all. With that in place the sneaky line no longer compiles, and tsc --strict says why:

error TS2353: Object literal may only specify known properties, and 'id' does not exist in type 'Partial<Omit<Article, "id">>'.

Record

Record<K, V> creates an object type with keys of type K and values of type V.

You could write { [key: string]: string } instead, and it would accept the same object. The reason to reach for Record with a union key is that it closes the set: Record<Status, string> requires an entry for every member of Status and rejects a key that is not one, so adding a fourth status breaks the table until you fill it in. An index signature does neither — it takes any string and misses nothing. Pick the one whose failure mode you want.

type Status = "pending" | "active" | "archived";

// Record creates a type with all Status keys
const statusLabels: Record<Status, string> = {
  pending: "Awaiting Review",
  active: "Currently Active",
  archived: "No Longer Active"
};

const statusColors: Record<Status, string> = {
  pending: "#FFA500",
  active: "#00FF00",
  archived: "#808080"
};

const statuses: Status[] = ["pending", "active", "archived"];
statuses.forEach(s => {
  console.log(`${statusLabels[s]} (${statusColors[s]})`);
});

// Record with dynamic keys
type UserScores = Record<string, number>;

const scores: UserScores = {
  alice: 95,
  bob: 87,
  charlie: 92
};

Object.entries(scores).forEach(([name, score]) => {
  console.log(`${name}: ${score}`);
});

Exclude, Extract, and NonNullable

Work with union types by filtering members. These three are the union-level counterparts of the key-level transformations above: same idea, applied to the members of a union rather than the properties of an object.

Before reading the results in the comments, predict two of them yourself: given AllTypes below, what does Exclude<AllTypes, null | undefined> leave, and what does NonNullable<AllTypes> leave? They land on the same answer here by two different routes — the standard library defines Exclude<T, U> as T extends U ? never : T and NonNullable<T> as T & {}, and only the first is a filter over union members.

type AllTypes = string | number | boolean | null | undefined;

// Exclude — remove types from a union
type Primitives = Exclude<AllTypes, null | undefined>;
// Result: string | number | boolean

// Extract — keep only matching types
type NumOrStr = Extract<AllTypes, string | number>;
// Result: string | number

// NonNullable — remove null and undefined
type Defined = NonNullable<AllTypes>;
// Result: string | number | boolean

// Practical example
type ApiResult = "success" | "error" | "loading" | "idle";
type ActiveState = Exclude<ApiResult, "idle">;

function handleState(state: ActiveState): string {
  switch (state) {
    case "success": return "Data loaded!";
    case "error": return "Something went wrong";
    case "loading": return "Please wait...";
  }
}

const states: ActiveState[] = ["loading", "success", "error"];
states.forEach(s => console.log(`${s}: ${handleState(s)}`));

ReturnType and Parameters

Extract types from function signatures.

function createUser(name: string, age: number, admin: boolean) {
  return { id: Math.random(), name, age, admin, createdAt: new Date() };
}

// Extract the return type
type User = ReturnType<typeof createUser>;

// Extract parameter types
type CreateUserParams = Parameters<typeof createUser>;

const user: User = createUser("Alice", 30, true);
console.log(`User: ${user.name}, admin: ${user.admin}`);

// Use with existing functions
function formatDate(date: Date, locale: string): string {
  return date.toLocaleDateString(locale);
}

type FormatDateReturn = ReturnType<typeof formatDate>; // string
type FormatDateParams = Parameters<typeof formatDate>; // [Date, string]

const args: FormatDateParams = [new Date(), "en-US"];
const result: FormatDateReturn = formatDate(...args);
console.log(`Formatted: ${result}`);

The form-builder pattern below shows several utility types working together — Record for the schema shape, a mapped type for the values, and Partial<Record<...>> for the errors. Two of the pieces are a preview: the { [K in keyof T]: ... } mapped type is covered in full after the capstone (Mapped Types), and satisfies FormSchema checks that the value conforms to FormSchema without widening its inferred type — so typeof schema still remembers each field's exact "text"/"number" literal. Read them for the shape; you'll learn to write them later. It runs as-is:

// Build a type-safe form builder
interface FormField {
  label: string;
  type: "text" | "email" | "number" | "select";
  required: boolean;
  options?: string[];
}

type FormSchema = Record<string, FormField>;

type FormValues<T extends FormSchema> = {
  [K in keyof T]: T[K]["type"] extends "number" ? number : string;
};

type FormErrors<T extends FormSchema> = Partial<Record<keyof T, string>>;

const schema = {
  name: { label: "Name", type: "text" as const, required: true },
  email: { label: "Email", type: "email" as const, required: true },
  age: { label: "Age", type: "number" as const, required: false },
  role: { label: "Role", type: "select" as const, required: true, options: ["admin", "user"] },
} satisfies FormSchema;

type MyFormValues = FormValues<typeof schema>;
type MyFormErrors = FormErrors<typeof schema>;

const values: MyFormValues = { name: "Alice", email: "alice@co.com", age: 30, role: "admin" };
const errors: MyFormErrors = { email: "Invalid email format" };

console.log("Form values:", JSON.stringify(values));
console.log("Form errors:", JSON.stringify(errors));

// Validate
Object.entries(schema).forEach(([key, field]) => {
  const value = values[key as keyof MyFormValues];
  if (field.required && !value) {
    console.log(`${field.label} is required`);
  } else {
    console.log(`${field.label}: ${value}`);
  }
});

Arrange the code

Reassemble a program that models the create → store → preview flow utility types describe: start from a draft (the shape an Omit<Activity, 'id'> create-DTO carries — no id yet), attach an id to make a full activity, pick just id and title into a preview (a Pick<Activity, 'id' | 'title'>), build a label from the preview, and log it. The lines are shuffled; put them back so the program runs and logs a1: Types. Then answer why this order: which line could not have been moved, and what does the runner do if you move it?

  1. const label = `${preview.id}: ${preview.title}`;
  2. const preview = { id: activity.id, title: activity.title };
  3. const draft = { title: "Types", minutesSpent: 30, archived: false };
  4. console.log(label);
  5. const activity = { ...draft, id: "a1" };
Continue learning

When an optional field is read as if it were there

Partial<T> is the most useful of these six and the easiest to misuse, because it changes something the running program has to cope with: every field may now be absent. The block below is that mistake, and its symptom is not a compiler complaint — it is a number.

Debug

applyPatch takes the current activity and a Partial<Activity> patch, adds any patched minutes to the running total, and keeps whatever the patch leaves out. Two calls exercise it: one patches the minutes, one patches only the title. The first call is fine. The second fails a check with a value that is not even a number. Commit a hypothesis about what the second call is actually adding to 30, then fix it so both checks pass.

import assert from "node:assert";

interface Activity {
id: string;
title: string;
minutesSpent: number;
}

// Intent: add a patch's minutes to the current total and keep any field the patch leaves out.
// A patch may carry any subset of the fields — that is what Partial<Activity> means.
function applyPatch(current: Activity, patch: Partial<Activity>): Activity {
return {
	id: current.id,
	title: current.title,
	minutesSpent: current.minutesSpent + patch.minutesSpent,
} as Activity;
}

const current: Activity = { id: "a1", title: "Types", minutesSpent: 30 };
const topUp = applyPatch(current, { minutesSpent: 15 });
const rename = applyPatch(current, { title: "Utility Types" });

assert.strictEqual(topUp.minutesSpent, 45, "a minutes patch should add to the current total, got " + topUp.minutesSpent);
assert.strictEqual(rename.minutesSpent, 30, "a patch with no minutes should leave the total alone, got " + rename.minutesSpent);
console.log(`top-up: ${topUp.minutesSpent} minutes`);
console.log(`rename: ${rename.minutesSpent} minutes`);

Expected output:

top-up: 45 minutes
rename: 30 minutes
Continue learning

Try It Yourself

Reading about utility types is not the same as deriving real DTOs with them. This is a build task: a small program that reports its own pass/fail. You finish a create/update/preview layer over an Activity entity — the exact create and update DTOs the tracker capstone derives from its entity type with Omit, Partial, and Pick. Run it as-is and it fails immediately, naming the first stub. Implement each until every check passes and it prints All checks passed.

The pieces reuse exactly what this lesson taught: Omit<Activity, "id"> for a create input that cannot carry a server-owned id, Partial<Omit<Activity, "id">> for an update patch that touches only some fields and never the id, and Pick<Activity, "id" | "title"> for a compact preview. The starter has the entity, the derived DTO types, and the checks — you write only the logic inside each function.

Build

Finish the build. Three functions are stubbed, and the checks below them fail until each returns the right value. Run it as-is to see which check fails first, decide what that function is missing, then implement them until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.

import assert from "node:assert";

// The entity is given. Do NOT change this.
interface Activity {
id: string;
title: string;
minutesSpent: number;
archived: boolean;
}

// A create DTO omits the server-owned id; an update DTO is a partial patch
// that can never touch the id; a preview keeps only id and title. These are
// DERIVED from Activity with utility types, not retyped by hand.
type CreateActivity = Omit<Activity, "id">;
type UpdateActivity = Partial<Omit<Activity, "id">>;
type ActivityPreview = Pick<Activity, "id" | "title">;

const nextId = (() => {
let n = 0;
return () => `a${++n}`;
})();

// TODO 1: build a full Activity from a CreateActivity by adding a fresh id.
//   Spread the input, then attach id: nextId(). The Omit<Activity, "id"> input
//   guarantees every OTHER field is already present, so no other defaults are needed.
//   create({ title: "Types", minutesSpent: 30, archived: false }).title -> "Types"
function create(input: CreateActivity): Activity {
// your code here
return { id: nextId(), title: "", minutesSpent: 0, archived: false }; // replace this
}

// TODO 2: apply an UpdateActivity patch onto an existing Activity, returning a NEW
//   object (do not mutate the original). Later fields win, so spread the patch LAST.
//   Only the keys present in the patch change; the id is untouchable by construction.
//   applyUpdate(act, { minutesSpent: 45 }).minutesSpent -> 45
function applyUpdate(current: Activity, patch: UpdateActivity): Activity {
// your code here
return current; // replace this
}

// TODO 3: build a compact preview of an activity — only its id and title —
//   matching the ActivityPreview (Pick) shape, so it stays in sync with Activity.
//   preview(act) -> { id: act.id, title: act.title }
function preview(act: Activity): ActivityPreview {
// your code here
return { id: "", title: "" }; // replace this
}

// --- Build checks: these must all pass. Do not edit below this line. ---
const made = create({ title: "Types", minutesSpent: 30, archived: false });
assert.strictEqual(made.title, "Types", "TODO 1: create should carry the title through from the CreateActivity input");
assert.strictEqual(made.minutesSpent, 30, "TODO 1: create should carry minutesSpent through");
assert.strictEqual(made.id, "a1", "TODO 1: create should attach a fresh generated id");

const patched = applyUpdate(made, { minutesSpent: 45 });
assert.strictEqual(patched.minutesSpent, 45, "TODO 2: applyUpdate should overwrite only the patched field");
assert.strictEqual(patched.title, "Types", "TODO 2: applyUpdate should leave unpatched fields unchanged");
assert.strictEqual(patched.id, made.id, "TODO 2: applyUpdate must never change the id");
assert.strictEqual(made.minutesSpent, 30, "TODO 2: applyUpdate must not mutate the original activity");

const p = preview(made);
assert.deepStrictEqual(p, { id: "a1", title: "Types" }, "TODO 3: preview should pick only id and title");

console.log("All checks passed.");
console.log("Created:", made.id, made.title);
console.log("Patched minutes:", patched.minutesSpent);
console.log("Preview:", JSON.stringify(preview(made)));

Expected output:

All checks passed.
Created: a1 Types
Patched minutes: 45
Preview: {"id":"a1","title":"Types"}
Continue learning

Once it passes, try two variations and predict each before running:

  1. Spread the patch first. In applyUpdate, change { ...current, ...patch } to { ...patch, ...current } so current is spread LAST. Predict which check fails first before running. Now the current object's fields overwrite the patch, so patched.minutesSpent stays 30 instead of becoming 45 — TODO 2's first check fires with AssertionError: TODO 2: applyUpdate should overwrite only the patched field and 30 !== 45. An instructive assert failure showing that in an object spread the LAST source wins, which is why the patch must come last.
  2. Patch the archived flag. After the checks pass, add const archivedActivity = applyUpdate(made, { archived: true }); console.log("Archived flag:", archivedActivity.archived, "| id unchanged:", archivedActivity.id === made.id); below the logs. Predict the new line before running. The Partial update lets you patch just archived while leaving everything else — including the id — alone, so you get Archived flag: true | id unchanged: true. This changes the echoed output, not any check, and is the payoff of the partial-patch DTO: change one field, keep the rest.

Capstone milestone

Milestone — the tracker service (the DTO layer). The tracker's service does not retype its entity for every operation: it DERIVES a create DTO (Omit the id), an update DTO (Partial of the mutable fields), and preview shapes (Pick) from one Activity type. The create/applyUpdate/preview layer you just built is that derivation. Confirm you can build type-safe DTOs from an entity with utility types instead of hand-maintaining parallel types.

Hint: This is the tracker-service milestone, shared with *Generics* (the generic Store<T>) and *Classes And OOP* (a class-based service). Here the load-bearing part is deriving DTOs from the entity with utility types — in the capstone these exact Omit/Partial/Pick shapes back the service's create and update methods.

  • Derived a create DTO with Omit<Activity, 'id'> so callers cannot supply a server-owned id
  • Derived an update DTO with Partial<Omit<Activity, 'id'>> — every field optional, id excluded
  • Derived a preview shape with Pick<Activity, 'id' | 'title'>
  • Kept one source of truth: the DTOs update automatically when the Activity entity changes
Continue learning

Transfer

Partial<Activity> turns every field into one that may be absent, and the reader has to cope with each one. Rust has no Partial, but it has Option<T> — a field declared Option<u32> either holds a number or holds None, and the compiler will not let you add it to anything until you have dealt with the None case. Both languages are solving the same problem with different machinery. Which statement names the invariant that holds across both, rather than a surface resemblance?

Continue learning

Key Takeaways

  • Partial<T> makes all properties optional — perfect for update functions
  • Pick<T, K> and Omit<T, K> select or exclude specific properties
  • Record<K, V> creates object types with specific key and value types
  • Exclude and Extract filter union type members
  • ReturnType and Parameters extract types from functions
  • Combine utility types for powerful transformations: Partial<Omit<T, "id">>

Pro Tip: When you find yourself defining a type that's almost identical to an existing one, reach for utility types first. Pick, Omit, and Partial eliminate redundancy and keep types in sync — when the source type changes, all derived types update automatically.

Next Steps

You can now derive precise types for every operation — but real programs also fail, and those failures need types too. Next, you'll learn TypeScript error handling: narrowing caught unknown errors, custom error classes, and the Result pattern that makes success and failure explicit in the return type — the validation layer the tracker's DTOs feed into.

Ready to continue? Head to Error Handling!

Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.