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 portPick 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?
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?
const label = `${preview.id}: ${preview.title}`;const preview = { id: activity.id, title: activity.title };const draft = { title: "Types", minutesSpent: 30, archived: false };console.log(label);const activity = { ...draft, id: "a1" };
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
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"}Once it passes, try two variations and predict each before running:
- Spread the patch first. In
applyUpdate, change{ ...current, ...patch }to{ ...patch, ...current }socurrentis spread LAST. Predict which check fails first before running. Now the current object's fields overwrite the patch, sopatched.minutesSpentstays30instead of becoming45— TODO 2's first check fires withAssertionError: TODO 2: applyUpdate should overwrite only the patched fieldand30 !== 45. An instructive assert failure showing that in an object spread the LAST source wins, which is why the patch must come last. - 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. ThePartialupdate lets you patch justarchivedwhile leaving everything else — including the id — alone, so you getArchived 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
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?
Key Takeaways
Partial<T>makes all properties optional — perfect for update functionsPick<T, K>andOmit<T, K>select or exclude specific propertiesRecord<K, V>creates object types with specific key and value typesExcludeandExtractfilter union type membersReturnTypeandParametersextract 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, andPartialeliminate 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!
Next lesson
Error Handling
Learn robust TypeScript error handling with try/catch, custom error classes, the Result pattern, and type narrowing for caught errors.
20 min