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.
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.
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 = Record string[]>; 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.
// ─── 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.
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 — 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.
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 = PickByValue string>; // { 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. 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.
// ─── 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 AddPrefix extends 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 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
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
4. Write function buildPath
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.
typeof runtime vs typetypeof 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.
Pick when you want OmitPick when you mean "all but password." Fix: Omit. Omit scales better when the type grows — new fields are included automatically.
typeof before ReturnTypeReturnType fails — createUser is a value, not a type. Fix: ReturnType. The compiler will tell you, but the error is confusing.
Record everywhereRecord allows any string key — no autocomplete, no typo protection. Fix: constrain the keys — Record<'admin' | 'user', User> or Record.
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.
type DeepPartial recurses infinitely on non-object types. Fix: add a conditional — T[K] extends object ? DeepPartial.
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.
Readonly for deep immutabilityReadonly 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
What is the difference between Pick and Omit?
Pick
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
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
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