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.
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.
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.
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
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.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.