Chapter 13 of 24 · TypeScript for Beginners — Complete Free Tutorial

Utility Types

In the TypeScript for Beginners series you can already design interfaces, unions and generics from scratch. Chapter 13 of this 24-chapter series introduces the utility types that TypeScript ships in the box: Partial, Pick, Record, ReturnType and half a dozen more. They are free, they add no runtime code, and once you know them you stop rewriting the same five type transforms in every project.

What utility types are

A utility type is a tiny generic type helper compiled into TypeScript's standard library. You never import them and never install them — a fresh project with "strict": true already knows every one of them. Three properties make them worth learning properly:

  • Pure types — they live only in the type system and vanish completely when the compiler emits JavaScript.
  • Generic — each one takes a type and produces a new type, so they compose: Partial<Omit<User, 'id'>> is an ordinary Tuesday.
  • Recognisable — they encode patterns nearly every codebase relies on, so a Pick in someone else's code reads instantly.

Copying, trimming and freezing

Three utilities change the modifiers of an object type without changing which properties exist: Partial<T> makes every property optional, Required<T> removes the optionals again, and Readonly<T> locks every property against assignment.

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

type UserUpdate = Partial<User>;       // every field becomes optional
type FullUser = Required<UserUpdate>;  // optionals removed again
type FrozenUser = Readonly<User>;      // every field becomes readonly
const update: UserUpdate = { name: 'Ada' };
const frozen: FrozenUser = { id: 'u1', name: 'Ada', email: 'ada@example.com', age: 36 };

frozen.name = 'Grace';                 // compile error
console.log(Object.keys(update));

Expected output: update holds only { name?: string | undefined }, FullUser resolves back to the full User, the assignment fails with error TS2540: Cannot assign to 'name' because it is a read-only property., and the log prints [ 'name' ].

Two details worth memorising. Required<T> strips undefined along with the ? marker, so Required<{ a?: string | null }> is { a: string | null } — a deliberate null survives. And Partial<T> is shallow: nested object properties keep their own shape, which surprises everyone the first time a nested field refuses to be optional.

Choosing keys: Pick, Omit and Record

Where Partial keeps every key, these three work with the key list itself. Pick<T, K> keeps only the keys you name, Omit<T, K> removes the keys you name, and Record<K, V> builds an object type from scratch keyed by a union of literals.

interface Product {
  id: string;
  title: string;
  price: number;
  stock: number;
}

type ProductCard = Pick<Product, 'id' | 'title' | 'price'>;
type ProductInput = Omit<Product, 'id' | 'stock'>;
type PriceMap = Record<'math' | 'science', number>;
const prices: PriceMap = { math: 88, science: 92 };

type Broken = Pick<Product, 'emial'>;   // typo in the key name

Expected output: ProductCard is { id: string; title: string; price: number }, ProductInput is { title: string; price: number }, prices.math is 88, and the typo fails with error TS2344: Type '"emial"' does not satisfy the constraint 'keyof Product'.

Filtering unions and reading signatures

The next group works on unions and functions instead of objects. Exclude<T, U> drops union members that match U; Extract<T, U> keeps only the matches; ReturnType<F> and Parameters<F> read a function's signature without you re-describing it.

type Status = 'draft' | 'review' | 'published';
type LiveStatus = Extract<Status, 'published'>;    // keeps matching members
type WorkingStatus = Exclude<Status, 'published'>; // removes matching members
type Mismatch = Extract<string, number>;           // nothing matches: never
function saveArticle(id: string, draft: boolean) {
  return { id, savedAt: Date.now(), draft };
}
type SaveResult = ReturnType<typeof saveArticle>;
type SaveArgs = Parameters<typeof saveArticle>;
type FormInput = string | null | undefined;
type SafeInput = NonNullable<FormInput>;            // null and undefined gone
type Delayed = Awaited<Promise<Promise<number>>>;  // promises unwrapped fully

Expected output: LiveStatus is 'published', WorkingStatus is 'draft' | 'review', Mismatch is never, SaveResult is { id: string; savedAt: number; draft: boolean }, SaveArgs is [id: string, draft: boolean], SafeInput is string and Delayed is number.

Which utility when?

The table below is the decision guide worth keeping open while you type. Each row answers one question: what shape am I trying to reach from here?

UtilityReach for it when…Example
Partial<T>Building an update object where any field may changePartial<User>
Required<T>Input arrived with optionals but you need them allRequired<SignupForm>
Readonly<T>The object must never be mutated (config, shared state)Readonly<Options>
Pick<T, K>You only need a few keys for a view or API payloadPick<User, 'id' | 'name'>
Omit<T, K>You need everything except secrets or generated idsOmit<User, 'passwordHash'>
Record<K, V>You need a lookup map keyed by a fixed set of literalsRecord<Role, string>
Exclude / ExtractYou are filtering a union of literal typesExclude<Status, 'draft'>
ReturnType / ParametersYou must wire a real function into a generic APIParameters<typeof save>
NonNullable<T>You are cleaning null and undefined out of inputNonNullable<string | null>
Awaited<T>You need the value inside a (possibly nested) promiseAwaited<Promise<User>>

Worked example: patching an API resource

Here is where utility types earn their keep. A REST API accepts a PATCH body: the same shape as the resource, but every field optional and the id off-limits. Composing Partial with Omit expresses that in one line, and the compiler enforces the rule for every caller.

interface Article {
  id: string;
  title: string;
  body: string;
  tags: string[];
  publishedAt: string | null;
}
// A client may send any subset of fields, but never the id
type ArticlePatch = Partial<Omit<Article, 'id'>>;
function applyPatch(article: Article, patch: ArticlePatch): Article {
  return { ...article, ...patch };
}
const article: Article = {
  id: 'a1',
  title: 'Learning TypeScript',
  body: 'A short guide to utility types.',
  tags: ['typescript'],
  publishedAt: null,
};
const next = applyPatch(article, {
  title: 'Learning TypeScript 5',
  tags: ['typescript', 'generics'],
});
console.log(next.title, next.tags.join(', '), next.publishedAt);
applyPatch(article, { id: 'a2' });   // compile error: ids are immutable

Expected output:

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

One spread produces the merged resource, one line of types blocks the illegal field, and nobody has to remember the rule in code review. That is the whole pitch for utility types: small compositions, enforced by the compiler.

Common mistakes

  • Treating Partial<T> as deep — it only touches the top level; nested objects keep their required fields, so write a recursive type (chapter 15) when you truly need depth.
  • Making everything optional “to be safe” — a return type full of optionals pushes undefined checks onto every caller; reserve Partial for input and patch shapes.
  • Forgetting Required<T> keeps null — it removes ? and undefined only; combine it with NonNullable when null must go too.
  • Misspelling keys in Pick and Omit — a typo fails with “does not satisfy the constraint keyof T”, which is the compiler doing you a favour; read it as such.
  • Re-describing signatures by hand — if you ever write [string, boolean] to mirror a function, reach for Parameters<typeof fn> so the copy can never drift.

Practice checklist

  1. Define interface Task { id: string; title: string; done: boolean; dueDate: string | null } and derive TaskPatch (Partial) and TaskPreview (Pick); hover each to see the resolved shape.
  2. Build a Record<'low' | 'medium' | 'high', string> of priority labels and index it once with labels['high'].
  3. Filter 'read' | 'write' | 'admin' with Exclude and Extract so only the read-only roles remain, then check the result with a hover.
  4. Take any two-argument function of your own and spread Parameters<typeof fn> into a different call.
  5. Write an applyPatch for a Product type and prove the compiler rejects a patch containing id.

What's next?

Utility types are the tools; the next three chapters show the machine that builds them. Learn to ask a type for its keys and to generate whole object types in keyof, Indexed Access and Mapped Types.

Quiz: Utility Types

5 questions · 70% to pass

Frequently Asked Questions

TypeScript is a strict superset of JavaScript that adds a static type system. You annotate the promises your data makes, the compiler checks every one before the code runs, then erases the types and emits ordinary JavaScript. You catch wrong arguments, typoed properties and forgotten null checks while you are still typing, and refactoring plus editor autocomplete both improve sharply.

You should. This series assumes you can already read and write JavaScript: functions, arrays and objects, closures, modules and async/await. TypeScript audits that knowledge rather than replacing it, and types cannot rescue code you do not understand. If those fundamentals feel shaky, work through the JavaScript for Beginners series on this site first, and this course will go much faster.

No. TypeScript is compiled, not interpreted: tsc parses your file, type-checks it, then erases every annotation and writes ordinary JavaScript. The browser and Node.js only ever run that emitted .js, so runtime behaviour, performance and debugging are unchanged. It also never validates API data at runtime, so the series teaches you to distrust every boundary that leaves the compiler.

If you already know JavaScript, TypeScript is a modest step rather than a new language: the syntax is the same plus a colon. Most beginners read their first red squiggle within days and feel comfortable in two to four weeks of regular practice. The curve steepens later around generics and advanced types, which is where this 24-chapter series deliberately spends its second half.

Both name the same object shapes, so either works for a simple contract. The differences decide the rest: only interfaces merge when declared twice, while only type aliases can express unions, primitives, tuples and mapped types. The series default is interface for props and exported shapes other files consume, and type for everything else. Pick one style per project and stay consistent.

strict: true is a bundle of safety switches, not a single behaviour. It turns on noImplicitAny, so every parameter must be inferrable, strictNullChecks, so null and undefined are their own types you must narrow, strictFunctionTypes, noImplicitThis, strictPropertyInitialization, and useUnknownInCatchVariables, which types catch values as unknown. Every example in this series compiles with it on.

any accepts anything and switches the compiler off, so one any spreads through inference into everything derived from it and you are back to dynamic typing with a .ts extension. unknown accepts anything too, but forbids property access until you narrow it with a typeof or instanceof check. The series pushes you toward unknown because it keeps the check you installed TypeScript to get.

Yes to both. Vite understands TypeScript natively: scaffold the vanilla-ts or react-ts template and a tsconfig.json is waiting for you, with moduleResolution set for the bundler. JSX belongs in .tsx files, props get an interface, and hooks infer or accept explicit type arguments. The series covers typed React in depth, from props, state and events to refs, context and fetch effects.

Enums still compile and work fine in TypeScript 5.x, but they are no longer the default recommendation. A union of string literals such as "light" | "dark" needs no runtime object, matches JSON directly and plays well with single-file transpilers like Vite's esbuild. Keep enum for existing codebases that already use it, avoid const enum, and prefer literal unions in new code.

Adopt it incrementally instead of rewriting. Install typescript, add a tsconfig.json with allowJs and noEmit so .js and .ts coexist while your bundler keeps building, then check individual files with a // @ts-check comment. Rename leaf modules to .ts one at a time, fix implicit any errors first, and gate every pull request with tsc --noEmit so untyped code cannot sneak back in.