Lesson 6

Utility types & advanced

Home › Courses › TypeScript › Lesson 6
🔷 Lesson 6 · 20 minutes

Utility types & advanced.
The vocabulary of type programming.

TypeScript ships with a set of built-in utility types that transform other types — Partial, Required, Pick, Omit, Record, Readonly, ReturnType, Parameters, Awaited. They eliminate the need to redeclare shapes. Underneath them are three operators that power everything: keyof, typeof, and mapped types. And on top sits the newest superpower — template literal types. This lesson covers all of it. By the end, you'll be able to read the source code of any library's types — because most of it is built from the operators in this lesson.

~20 minutes Advanced TypeScript 5 · utility types
TypeScript path Essential utility types ReturnType & Parameters keyof & typeof Mapped types Template literals Challenge

Utility types transform one type into another. Partial makes properties optional; Required makes them required; Pick keeps keys; Omit removes keys; Record builds a dictionary; Readonly marks fields readonly. Advanced operators: keyof T gives the union of T's keys; typeof value extracts a value's type; mapped types ([K in keyof T]) iterate over keys to build new types; template literal types (on${string}`) type string patterns. These are what every library is built from.

Before this lesson: complete Lesson 5 — you'll use generics, constraints, and interfaces throughout.

🧰 The essential utility types

The nine types you'll use in every real project.

Utility types are built-in generics — TypeScript provides them, you use them. They transform one type into a variant of itself. No runtime cost: everything happens at compile time and disappears in the output.

utility-types.ts
interface User {
id: string;
name: string;
email: string;
password: string;
age?: number;
}

// ─── Partial: all properties optional ────────
type PartialUser = Partial;
// { id?: string; name?: string; email?: string; password?: string; age?: number }
const patch: PartialUser = { name: 'Alice' }; // ✅ partial update

// ─── Required: all properties required ───────
type FullUser = Required;
// age is now required (number, not number | undefined)

// ─── Pick: keep only these keys ──────────────
type UserPreview = Pick'id' | 'name'>;
// { id: string; name: string }
const preview: UserPreview = { id: '1', name: 'Alice' };

// ─── Omit: remove these keys ─────────────────
type SafeUser = Omit'password'>;
// { id, name, email, age } — everything except password

// ─── Record: dictionary with typed keys ──────
type Role = 'admin' | 'user' | 'guest';
type Permissions = Recordstring[]>;
const perms: Permissions = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read']
// ❌ Missing 'guest' key = compile error (Record enforces all keys)
};

// ─── Readonly: mark all properties readonly ──
type FrozenUser = Readonly;
const fu: FrozenUser = { id: '1', name: 'A', email: 'a@x', password: 'x' };
// fu.name = 'B'; // ❌ Cannot assign to 'name' (read-only)

console.log(patch, preview, perms);
// Every utility type is erased at compile time.
// The output has no Partial, Required, Pick, Omit, Record, or Readonly.
// Only the runtime values remain — same as plain JS.

const patch = { name: 'Alice' };

const preview = { id: '1', name: 'Alice' };

const perms = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read']
};

const fu = { id: '1', name: 'A', email: 'a@x', password: 'x' };

console.log(patch, preview, perms);
{ name: 'Alice' }
{ id: '1', name: 'Alice' }
{
admin: [ 'read', 'write', 'delete' ],
user: [ 'read', 'write' ],
guest: [ 'read' ]
}

Partial

Makes every property optional (?). Perfect for PATCH endpoints and "update" payloads where any subset of fields can be sent.

Required

Makes every property required (removes ? and undefined). Useful when a type has optional fields but a specific function needs all of them.

Pick / Omit

Whitelist vs blacklist. Pick keeps only specified keys. Omit removes specified keys. Both produce a new type — no runtime cost.

Record

Builds a dictionary: keys are K, values are V. Far safer than { [key: string]: V } because it constrains both keys and values — and enforces that all keys are present.

Readonly

Marks every property as readonly at compile time. Erased at runtime — the object is still mutable in JS. For deep immutability use as const or Object.freeze.

Omit is more common than Pick in practice. When exposing a user to the frontend, you omit password rather than picking every other field. When creating a request from a full entity, you omit id and createdAt. When building a public API, you omit internal fields. The pattern is "I have X, I want a version of X without these specific things" — which is Omit. Reach for Pick only when you want a genuinely minimal subset (e.g. a search result, a list item).

🔬 ReturnType, Parameters, Awaited — extracting types

Pull types out of functions instead of redeclaring them.

These utility types extract information from function signatures. Instead of duplicating a type in two places (the function and the variable that holds its result), you extract it with ReturnType. This keeps types in sync — one change to the function updates every dependent type.

extract.ts
// ─── ReturnType: extract the return type ────
function createUser(name: string, email: string) {
return {
id: crypto.randomUUID(),
name,
email,
createdAt: new Date()
};
}

type User = ReturnType<typeof createUser>;
// { id: string; name: string; email: string; createdAt: Date }
// No duplication — the function defines the shape, ReturnType pulls it out.

const u: User = createUser('Alice', 'a@x.com');

// ─── Parameters: extract param types ─────────
type CreateUserArgs = Parameters<typeof createUser>;
// [string, string]

const args: CreateUserArgs = ['Bob', 'b@x.com'];
// const bad: CreateUserArgs = ['x']; // ❌ needs 2 elements

// ─── Works with function types too ───────────
type Handler = (event: MouseEvent, x: number) => boolean;
type HandlerReturn = ReturnType; // boolean
type HandlerArgs = Parameters; // [MouseEvent, number]

// ─── Awaited: unwrap promises ────────────────
async function fetchUser(id: string): Promise {
return { id, name: 'x', email: 'x', createdAt: new Date() };
}

type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;
// User — unwraps the Promise

// ─── NonNullable: remove null/undefined ──────
type MaybeUser = User | null | undefined;
type SafeUser = NonNullable;
// User

// ─── The pattern: sync types by extraction ───
// Bad: function f() { return { a: 1 }; } type R = { a: number }; // duplicate
// Good: function f() { return { a: 1 }; } type R = ReturnType; // synced

console.log(u.name, args, perms);
// All type extraction erased. Only the runtime functions and values remain.

function createUser(name, email) {
return {
id: crypto.randomUUID(),
name,
email,
createdAt: new Date()
};
}

const u = createUser('Alice', 'a@x.com');

const args = ['Bob', 'b@x.com'];

async function fetchUser(id) {
return { id, name: 'x', email: 'x', createdAt: new Date() };
}

console.log(u.name, args, perms);
Alice [ 'Bob', 'b@x.com' ] {
admin: [ 'read', 'write', 'delete' ],
user: [ 'read', 'write' ],
guest: [ 'read' ]
}

ReturnType

Extracts the return type of a function. Use typeof fn to get the function's type first: ReturnType. The pattern for "reuse the type of what this returns."

Parameters

Extracts the parameter types as a tuple. Parameters gives [arg1Type, arg2Type]. Useful for wrapping functions and forwarding arguments.

Awaited

Unwraps a Promise. Awaited> = User. Works with nested promises and async function returns. Combine with ReturnType to get the resolved value of an async function.

NonNullable

Removes null and undefined from a union. NonNullable = User. Useful when you know a value is present but the type still carries the nullable variants.

Use ReturnType as your default for API types. When you write an API client, define the function first (function getUser(id): Promise), then extract the types rather than declaring them upfront. If the function changes, every dependent type follows automatically. This is the pattern React codebases use for hooks: type UseUserResult = ReturnType. It's also how Next.js, tRPC, and React Query propagate types through the whole stack — a single function definition flows into components, hooks, and even the client.

🔑 keyof and typeof — the introspection operators

Turn types into values (at type level) and values into types.

keyof T produces a union of T's keys. typeof value produces the type of a runtime value. Together they unlock the pattern of writing functions that accept any object and any of its keys, with full type safety on both the object and the key.

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

// ─── keyof: union of property names ──────────
type UserKey = keyof User;
// 'id' | 'name' | 'email' | 'age'

const k: UserKey = 'name'; // ✅
// const bad: UserKey = 'foo'; // ❌ 'foo' not a key of User

// ─── The get pattern ─────────────────────────
function getextends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const user: User = { id: '1', name: 'Alice', email: 'a@x', age: 28 };
const name = get(user, 'name'); // string
const age = get(user, 'age'); // number
// get(user, 'foo'); // ❌ 'foo' is not a key of User

// ─── typeof: value → type ───────────────────
const config = {
host: 'localhost',
port: 3000,
debug: true
};

type Config = typeof config;
// { host: string; port: number; debug: boolean }

function start(c: Config): string {
return ``${c.host}:{c.debug ? ' (debug)' : ''}`;
}

// ─── typeof on functions ─────────────────────
function greet(name: string): string {
return Hi, ${name}`;
}

type GreetFn = typeof greet;
// (name: string) => string

// ─── keyof typeof: keys from a value ─────────
type ConfigKey = keyof typeof config;
// 'host' | 'port' | 'debug'

console.log(name, age, k, start(config));
// keyof and typeof (in type position) are erased.
// The runtime is plain JavaScript — no type introspection.

const k = 'name';

function get(obj, key) {
return obj[key];
}

const user = { id: '1', name: 'Alice', email: 'a@x', age: 28 };
const name = get(user, 'name');
const age = get(user, 'age');

const config = {
host: 'localhost',
port: 3000,
debug: true
};

function start(c) {
return ``${c.host}:{c.debug ? ' (debug)' : ''}`;
}

function greet(name) {
return Hi, ${name}`;
}

console.log(name, age, k, start(config));
Alice 28 name localhost:3000 (debug)

keyof T — the key union

Produces a union of T's property names. keyof User = 'id' | 'name' | 'email' | 'age'. Use it to constrain generics (K extends keyof T) or index into a type (T[K]).

typeof value — the type

In a type position, extracts the type of a runtime value. typeof config is the inferred type of the config object. Works with variables, functions, imports.

keyof typeof

Combines both: keyof typeof config = 'host' | 'port' | 'debug'. The idiomatic way to extract keys from a runtime object. Common in config objects and route definitions.

The get pattern

function get(obj: T, key: K): T[K] — the typed property accessor. Returns the exact type of the property: string for 'name', number for 'age'. Every typed getter uses this.

typeof is two operators in one keyword. In expression position (typeof x === 'string') it's the runtime JavaScript operator, returning a string. In type position (type T = typeof x) it's the TypeScript type operator, returning a type. The compiler decides which based on context. This is confusing at first — remember: type position = type operator, expression position = runtime operator.

🗺️ Mapped types — building types from types

How Partial, Required, and Readonly are actually implemented.

A mapped type iterates over a union of keys to build a new object type. The syntax { [K in Keys]: ValueType } says "for each key K in Keys, define a property with type ValueType." This is the foundation of every utility type, and it lets you generate types programmatically.

mapped.ts
interface User {
id: string;
name: string;
age: number;
}

// ─── These are how utilities are actually built ───
type MyPartial = { [K in keyof T]?: T[K] };
type MyRequired = { [K in keyof T]-?: T[K] };
type MyReadonly = { readonly [K in keyof T]: T[K] };

// MyPartial = { id?: string; name?: string; age?: number }

// ─── Modifiers: + and - ─────────────────────
// ? adds optional
// -? removes optional (Required)
// readonly adds readonly
// -readonly removes readonly

type Mutable = { -readonly [K in keyof T]: T[K] };
// Removes readonly from every field (the inverse of Readonly)

// ─── Change value types via mapped type ──────
type Stringify = { [K in keyof T]: string };
// { id: string; name: string; age: string }

type Nullable = { [K in keyof T]: T[K] | null };
// { id: string | null; name: string | null; age: number | null }

// ─── Map over a subset of keys ───────────────
type PickByValue = {
[K in keyof T as T[K] extends V ? K : never]: T[K]
};
// Keeps only keys whose values extend V
type StringKeys = PickByValuestring>;
// { id: string; name: string } — age filtered out

// ─── Conditional value mapping ───────────────
type DeepReadonly = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly : T[K]
};

const p: MyPartial = { name: 'Alice' };
const sk: StringKeys = { id: '1', name: 'A' };

console.log(p, sk);
// Mapped types are erased entirely. The runtime sees only the values.

const p = { name: 'Alice' };
const sk = { id: '1', name: 'A' };

console.log(p, sk);
{ name: 'Alice' } { id: '1', name: 'A' }

[K in keyof T]

Iterates over T's keys. K is bound to each key in turn. Inside the body, T[K] gives the value type for that key. This is the core syntax.

Modifiers: ? / -?

Adds or removes optional. ?: makes optional (Partial). -?: removes optional (Required). Same for readonly / -readonly.

as key remapping

[K in keyof T as SomeType] transforms the key. as T[K] extends V ? K : never filters out keys. This is how PickByValue and many other advanced types work.

Recursive mapped types

DeepReadonly recurses into nested objects. Mapped types can recurse — this is how libraries build deep transformations like DeepPartial, DeepReadonly.

Read the TypeScript source — it's surprisingly readable. Every utility type in lib.es5.d.ts is a mapped type of 1-3 lines. Open the source and search for "Partial" — you'll see type Partial = { [P in keyof T]?: T[P] };. Once you understand mapped types, you can read the source of every utility type and — crucially — write your own when none of the built-ins fit. This is where TypeScript stops feeling like "magic syntax" and starts feeling like a language you can extend.

✨ Template literal types — string patterns at the type level

Model string shapes precisely. Catch typos in event names, routes, keys.

Template literal types use template string syntax at the type level. Hello,${string}` matches any string starting with "Hello, ". They compose with utilities like Capitalize, Lowercase, Uppercase. The result: you can model event names, route patterns, and dotted paths without a runtime validation library.

template.ts
// ─── Basic template literal type ─────────────
type Greeting = Hello, ${string}`;

const g1: Greeting = 'Hello, Alice'; // ✅
const g2: Greeting = 'Hello, world'; // ✅
// const bad: Greeting = 'Hi, Alice'; // ❌ must start with 'Hello, '

// ─── Event names ─────────────────────────────
type Event = 'click' | 'focus' | 'blur';
type Handler = on${Capitalize}`;
// 'onClick' | 'onFocus' | 'onBlur'

const h: Handler = 'onClick'; // ✅
// const bad: Handler = 'onScroll'; // ❌ not a known event

// ─── Route patterns ──────────────────────────
type Route = /users/${string}/posts/${string};

const r1: Route = '/users/123'; // ✅
const r2: Route = '/posts/abc'; // ✅
// const bad: Route = '/comments/1'; // ❌ not in the union

// ─── Dot-path keys ───────────────────────────
interface Config {
host: string;
port: number;
}
type Dotted = {
[K in keyof T as ${K & string}.` extends infer P ? never : K]: T[K]
};

// ─── Template literal as a transform ─────────
type AddPrefixextends string> = {
[K in keyof T as ``${P}${K & string}]: T[K]
};

interface Fields {
name: string;
age: number;
}
type FormFields = AddPrefix'form'>;
// { formname: string; formage: number }

// ─── Real-world: typed API paths ─────────────
type Entity = 'user' | 'post' | 'comment';
type ApiPath = /api/${Entity}/api/{string}`;
// '/api/user' | '/api/user/123' | '/api/post' | '/api/post/abc' | ...

const path: ApiPath = '/api/user/42';

console.log(g1, h, r1, path);
// Template literal types are erased. Runtime is plain strings.

const g1 = 'Hello, Alice';
const g2 = 'Hello, world';

const h = 'onClick';

const r1 = '/users/123';
const r2 = '/posts/abc';

const path = '/api/user/42';

console.log(g1, h, r1, path);
Hello, Alice onClick /users/123 /api/user/42

The syntax

prefix${T}suffix just like runtime template strings. The compiler matches the shape.

Capitalize utilities

Capitalize, Lowercase, Uppercase, Uncapitalize transform literal string types. Combined with unions, they generate precise patterns.

Routing and events

Type your routes as /users/${string}on${Capitalize<Event>}. Typos fail to compile, and autocomplete suggests valid patterns.

Key remapping with as

[K in keyof T as ${P}${K & string}] — transform every key by adding a prefix. This is how React Query, Zod, and many libraries type their derived shapes.

Template literal types are the modern replacement for enums in string-heavy domains. Instead of an enum, use type Event = 'click' | 'focus', then type Handler = on${Capitalize}` to derive the handler names. No runtime enum, no imports, and the compiler checks both. This is the pattern that Next.js uses for route names, that React Query uses for query keys, and that tRPC uses for its entire API surface. Once you see it, you'll use it in every string-typed API you design.

🏆 Challenge

Four tasks. Each exercises a different advanced feature.

Task 1 — Build your own Partial and Required

Without using TypeScript's built-in utilities, write from scratch:

1. MyPartial — makes every property optional
2. MyRequired — makes every property required
3. MyReadonly — marks every property readonly
4. MyMutable — removes readonly (use -readonly)

Test each against interface User { id: string; name?: string } and verify in VS Code that the resulting types behave exactly like the built-ins. This is the exercise that demystifies "how does Partial actually work?" — it's a mapped type, and you just wrote it.

Task 2 — Extract return types for an API layer

Build a mini API layer using type extraction:

1. Define function fetchUser(id: string): Promise
2. Define function fetchPosts(userId: string): Promise
3. Extract types with ReturnType and Awaited: type UserResult = Awaited>
4. Build a generic wrapResponse Promise> type that wraps the awaited result in { data: T; error: null } | { data: null; error: Error }
5. Confirm the type flows correctly at each step

The point: this is how React Query, SWR, and every modern data-fetching library types their internals. Once you can extract and combine function types, you can type any async API.

Task 3 — Typed config with keyof typeof

Given a runtime config object:

const config = { apiUrl: 'https://x.com', timeout: 3000, retries: 5 }

1. Extract its type with typeof config
2. Extract its keys with keyof typeof config
3. Build a typed getConfig(key: K): typeof config[K]
4. Call it with 'apiUrl', 'timeout', 'retries' and verify each returns the precise type
5. Call it with 'nonexistent' and confirm the compile error

The point: the config object is the single source of truth. Its type, its keys, and its value types all flow from one declaration. No duplication. This is how typed config systems work at every scale.

Task 4 — Template literals for API paths

Build a typed API client path system:

1. type Entity = 'user' | 'post' | 'comment'
2. type Id = ``${E}_{E}|/api/{Id}`
4. Write function buildPath(entity: E, id?: Id): ApiPath
5. Test: buildPath('user', 'user_123') → '/api/user/user_123'
6. Try buildPath('user', 'post_456') — should fail (post ID is wrong type for user path)
7. Try buildPath('comment', 'comment_789') — should succeed

The point: this is exactly how typed API clients (tRPC, ts-rest, OpenAPI-derived clients) work. The compiler enforces that entity names, IDs, and paths match — no runtime validation needed.

⚠️ Common mistakes

The utility type and advanced operator bugs that catch every beginner.

1. Confusing typeof runtime vs type

typeof x === 'string' is the runtime operator (returns a string). type T = typeof x is the type operator (returns a type). Fix: context decides — inside expressions = runtime, inside type positions = type-level.

2. Using Pick when you want Omit

Pick when you mean "all but password." Fix: Omit. Omit scales better when the type grows — new fields are included automatically.

3. Forgetting typeof before ReturnType

ReturnType fails — createUser is a value, not a type. Fix: ReturnType. The compiler will tell you, but the error is confusing.

4. Using Record everywhere

Record allows any string key — no autocomplete, no typo protection. Fix: constrain the keys — Record<'admin' | 'user', User> or Record.

5. Thinking mapped types exist at runtime

Mapped types, keyof, and typeof at the type level are all compile-time only. Fix: never write runtime logic that depends on them — they're erased.

6. Infinite recursion in mapped types

type DeepPartial = { [K in keyof T]: DeepPartial } recurses infinitely on non-object types. Fix: add a conditional — T[K] extends object ? DeepPartial : T[K].

7. Over-constraining template literal types

type R = /${'user' | 'post'}/${string} — the union doesn't distribute correctly in some positions. Fix: sometimes you need a conditional to distribute, or restructure the union.

8. Using Readonly for deep immutability

Readonly is shallow — nested objects are still mutable. Fix: use as const for literals, or write a recursive DeepReadonly. Or Object.freeze at runtime.

❓ Frequently asked questions

What are utility types in TypeScript?

Utility types are built-in generic types that transform other types. Partial makes all properties optional. Required makes them all required. Pick keeps only specified keys. Omit removes specified keys. Record builds a dictionary. Readonly marks everything readonly. ReturnType extracts a function's return type.

What is the difference between Pick and Omit?

Pick keeps only the keys you specify — a whitelist. Omit removes the keys you specify — a blacklist. Pick produces a type with just id and name. Omit produces a User without password. Use Pick when you want to select a few known fields. Use Omit when you want most fields and want to remove a few.

What is the keyof operator?

keyof T produces a union of T's property names. If User = { id: string; name: string }, then keyof User = 'id' | 'name'. This is the foundation of type-safe property access: function get(obj: T, key: K): T[K].

What is the typeof operator in type position?

In a type position, typeof extracts the type of a value. const user = { id: '1', name: 'Alice' }; type User = typeof user; — User is now the inferred shape of user. Not to be confused with the runtime typeof operator — same keyword, different context.

What are mapped types?

Mapped types iterate over a union of keys to build a new object type. type Optional = { [K in keyof T]?: T[K] }; — this is how Partial is implemented. Mapped types use the [K in Keys] syntax and can add or remove modifiers (readonly, optional).

What are template literal types?

Template literal types use string template syntax at the type level. type Greeting = Hello, ${string}— this type matches any string starting with 'Hello, '. They can build complex string patterns: type EventName =on${Capitalize<string>}. Commonly used to type event handler names, route patterns, and enum-like keys.

What is Record used for?

Record builds an object type where keys are K and values are V. type Roles = Record<'admin' | 'user' | 'guest', boolean> produces { admin: boolean; user: boolean; guest: boolean }. It's the cleanest way to build dictionaries, config maps, and lookup tables.

What is ReturnType and when is it useful?

ReturnType extracts the return type of a function type. type UserReturn = ReturnType — UserReturn is whatever getUser returns. This is useful when you don't want to duplicate the return type declaration: let the function define it, then extract it with ReturnType.

🔗 Continue your TypeScript path

🌐 External resources

🔧 Related problem solvers

← Back to TypeScript track
You're in the TypeScript track · Lesson 6