intermediate22 min

Utility Types

Derive new types from existing ones with Partial, Pick, Omit, Record, and Readonly.

What you'll learn

  • Derive a type from another with Partial, Pick, and Omit
  • Describe a lookup object with Record
  • Explain why deriving beats duplicating a shape

Prerequisites

Explanation

Once you have a shape, you usually need variations of it: the same thing with every field optional for an update, or only two of its fields for a summary. Writing those out by hand means they drift apart the moment the original changes.

Utility types derive one type from another. They are ordinary generic types that ship with TypeScript.

Partial<T> — every field optional

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

function updateUser(id: string, changes: Partial<User>) { /* … */ }

updateUser("u1", { name: "Ada" }); // fine — other fields not required

Exactly right for a patch operation, where sending only what changed is the point.

Pick<T, Keys> and Omit<T, Keys>

type UserSummary = Pick<User, "id" | "name">;   // { id, name }
type UserWithoutId = Omit<User, "id">;          // { name, email }

The keys are given as a union of literal types — which is the payoff from lesson 5. Misspelling a key is a compile error, and renaming a field on User immediately flags every derived type that referenced the old name.

Choosing between them is about intent and durability: Pick when the list is short and stable, Omit when you want everything except a couple of fields and expect new fields to be included automatically as they are added.

Record<Keys, Value>

Describes an object used as a lookup:

type Status = "draft" | "published";
const labels: Record<Status, string> = {
  draft: "Not ready",
  published: "Live",
};

Because Status is a literal union, Record requires every key. Add "archived" to Status and this object immediately fails to compile until you handle it — a small example of making an illegal state unrepresentable.

Readonly<T>

const config: Readonly<User> = { id: "1", name: "Ada", email: "a@b.c" };
config.name = "Grace"; // rejected

Compile-time only. Nothing stops mutation at runtime; the guarantee is that your code will not compile if it tries.

The principle

Derive, do not duplicate. One source of truth means one place to change, and the compiler propagates the consequences everywhere.

Example

One interface, four derived types, each still linked to the original.

interface Article {
  id: string;
  title: string;
  body: string;
  published: boolean;
}

type ArticleSummary = Pick<Article, "id" | "title">;
type ArticleDraft = Omit<Article, "id">;
type ArticlePatch = Partial<Article>;

type Status = "draft" | "published";
const statusLabels: Record<Status, string> = {
  draft: "Not ready",
  published: "Live",
};

const summary: ArticleSummary = { id: "a1", title: "Utility Types" };
const patch: ArticlePatch = { published: true };

console.log(summary.title);
console.log(statusLabels.draft, statusLabels.published);
console.log(JSON.stringify(patch));

Try it yourself

Add "archived" to Status and Run. Record forces you to supply a label for it before the code compiles.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Define `interface Product { id: string; name: string; price: number }`. Derive `type ProductPatch = Partial<Product>`. Write `applyPatch(p: Product, patch: ProductPatch): Product` returning a new product with the patch applied.

Checks: applyPatch is defined · Applies only the patched field · plus 1 hidden check

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Define `type Level = "low" | "high"` and a `Record<Level, number>` called `thresholds` with low = 10 and high = 100. Then write `thresholdFor(level: Level): number` returning the matching number.

Checks: thresholds maps both levels · thresholdFor is defined · thresholdFor returns the matching value

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Hand-writing a second interface that duplicates most of the first. It drifts the moment either changes.
  • Expecting `Readonly<T>` to prevent mutation at runtime. It is erased like every other type.
  • Using `Record<string, T>` where a literal union would force exhaustive keys and catch missing cases.

Knowledge check

Knowledge check

1. What does `Partial<User>` produce?
2. What happens if you add "archived" to `type Status` used in `Record<Status, string>`?
3. When is `Omit` preferable to `Pick`?

Takeaway

Derive types from one source of truth so a change in the original propagates everywhere instead of drifting.

Summary

`Partial`, `Pick`, `Omit`, `Record`, and `Readonly` build new types from existing ones. Combined with literal unions, `Record` enforces exhaustive keys — turning a forgotten case into a build error.

References

Your notes

Notes save automatically.

Finished this lesson?

Mark it complete to track your progress and schedule a future review.