Utility Types
TypeScript provides built-in utility types that transform existing types. These let you create new types from old ones without repetition.
Partial and Required
Partial<T> makes all properties optional. Required<T> makes all properties required.
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:
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>'.
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.
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
type ArticleUpdate = Partial<Omit<Article, "id">>;
const update: ArticleUpdate = { title: "Updated Title" };
console.log(`Update: ${JSON.stringify(update)}`);
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?
Record
Record<K, V> creates an object type with keys of type K and values of type V.
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.
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. Each const consumes the binding above it, so only one order runs top-to-bottom and logs a1: Types.
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" };
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
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