Utility & Mapped Types
This is TypeScript's type-level transformation layer: deriving new types from existing ones instead of re-declaring shapes by hand. Interviewers use it to separate people who consume types from people who can compute them — from Partial and Omit up through mapped, conditional, and template literal types.
part ofTypeScriptoverview, primer and where to startread it →on this pageshowhide
explore
- Built-in Utility Types23 questions
- Partial, Required & Readonly5 questions
- Pick & Omit4 questions
- Record4 questions
- Exclude, Extract & NonNullable4 questions
- ReturnType, Parameters & Awaited6 questions
- Mapped Types12 questions
- keyof, in & Index Access4 questions
- Key Remapping with as4 questions
- Modifier Add & Remove (+/-)4 questions
- Conditional Types14 questions
- extends Conditions4 questions
- infer & Type Extraction5 questions
- Template Literal Types5 questions
- Recursive Types & Deep Transformations4 questions
- When Type-Level Programming Hurts5 questions
questions
63 · 6 sectionsIn TypeScript, what do the built-in utility types `Exclude<T, U>`, `Extract<T, U>` and `NonNullable<T>` produce when you apply them to a union type?
basics
~10 sThey are set operations over unions. Exclude<T, U> drops every member of T assignable to U, Extract<T, U> keeps only those members, and NonNullable<T> removes null and undefined from T.
In TypeScript, a function `createUser` returns an object literal whose shape you never declared. How do you get a named type for that return value without writing the shape twice, and why does `ReturnType<createUser>` fail to compile?
basics
~10 sReturnType<typeof createUser> gives the type. In a type position, typeof turns the value createUser into its function type, which ReturnType then unwraps. ReturnType<createUser> fails because createUser is a value, not a type.
In TypeScript, what does each of the built-in utility types `Partial<T>`, `Required<T>` and `Readonly<T>` do to the properties of T?
basics
~20 sPartial<T> makes every property of T optional, Required<T> makes every property mandatory, and Readonly<T> makes every property read-only. All three keep T's keys and property types, flip one modifier, and disappear when the code is compiled to JavaScript.
In TypeScript, what do `Pick<T, K>` and `Omit<T, K>` produce, and why derive a type with them instead of hand-writing a second interface?
basics
~20 sPick<T, K> builds an object type containing only the properties of T named in K; Omit<T, K> contains every property of T except those. Deriving keeps one source of truth, so editing T updates both types automatically.
In TypeScript, what object type does the built-in `Record<K, V>` utility produce, and how does its mapped-type definition explain the difference between `Record<string, number>` and `Record<'a' | 'b', number>`?
basics
~20 sRecord<K, V> builds an object type whose keys come from K and whose values are all V. It is defined as the mapped type { [P in K]: V }, so a string key produces an open index signature while a union of literals produces exactly those required properties.
In TypeScript, what does the `keyof` operator produce for an object type such as `interface User { id: number; name: string }`, and how do you apply it to a value like `const config = { host: 'localhost', port: 5432 }`?
basics
~20 skeyof produces the union of a type's property keys as literal types: keyof User is "id" | "name". It operates on types only, so for a value you write keyof typeof config. Nothing of it survives compilation.
In TypeScript, given `interface User { id: number; name?: string }` and `const roles = ['admin', 'user'] as const`, what types do the indexed access types `User['id']`, `User[keyof User]` and `(typeof roles)[number]` produce?
basics
~20 sIndexed access reads a property's type by key: User['id'] is number, User[keyof User] is the union of all property types (number | string | undefined), and (typeof roles)[number] is the union of the tuple's elements, 'admin' | 'user'.
In TypeScript, how do you write a `Mutable<T>` mapped type that strips `readonly` from every property of `T`, and what do the `+` and `-` prefixes mean in a mapped type?
basics
~20 sWrite type Mutable<T> = { -readonly [K in keyof T]: T[K] }. In a mapped type the minus prefix strips a modifier the source property carried and plus adds one; both prefixes apply to readonly and to the optional marker ?.
In TypeScript, given `type A = { id: number; name: string }` and `type B = { id: number; age: number }`, what is `keyof (A | B)`, and what rule produces that answer?
basics
~20 skeyof (A | B) is "id" — only the keys every member has. keyof distributes over the union and the results are intersected, because a value of a union type is guaranteed to carry just the shared properties. The mirror case: keyof (A & B) is "id" | "name" | "age".
In a TypeScript mapped type, what does the `as` clause in `{ [K in keyof T as NewKey]: T[K] }` do, and what happens to a key whose `as` clause evaluates to `never`?
basics
~20 sThe as clause renames each key while the mapped type iterates, emitting the property under the computed name instead of K. A key whose clause evaluates to never produces no property at all, which is how mapped types filter keys out.
In TypeScript, what does the type `type IsString<T> = T extends string ? 'yes' : 'no'` do, and what does `IsString<'abc'>` resolve to?
basics
~20 sA conditional type chooses between two types by testing assignability: if T is assignable to string the type becomes 'yes', otherwise 'no'. IsString<'abc'> resolves to 'yes', because the literal type 'abc' is assignable to string.
In TypeScript, given `type ToArray<T> = T extends unknown ? T[] : never`, why does `ToArray<string | number>` evaluate to `string[] | number[]` rather than `(string | number)[]`?
basics
~20 sA conditional type whose checked type is a bare type parameter distributes: TypeScript applies it to each union member separately and unions the results. ToArray runs once on string and once on number, producing string[] | number[].
In a TypeScript conditional type, `extends` is often described as "assignable to" rather than "inherits from". What does `type R = { id: number; name: string } extends { id: number } ? 'yes' : 'no'` resolve to, and why?
basics
~20 sThe result is 'yes'. In a conditional type, extends asks whether the left type is assignable to the right one, and an object type with an extra property is assignable to one that requires fewer. No declared inheritance is involved.
In a TypeScript conditional type, what does the `infer` keyword do, where may the variable it declares be used, and how would you write an `ElementType<T>` that pulls the element type out of an array type?
basics
~20 sinfer declares a type variable inside the extends clause of a conditional type, letting the compiler pattern-match a shape and capture whatever sits in that slot. The captured variable is in scope only in the true branch.
In TypeScript, `type IsString<T> = T extends string ? true : false` gives `boolean` for `IsString<string | number>`. Explain that result and how you would make the check apply to the whole union instead.
basics
~20 sThe conditional distributes over the union: string yields true, number yields false, and true | false is exactly how boolean is defined. Wrapping both sides in one-element tuples, [T] extends [string], disables distribution and returns false.
In TypeScript, what values does the type `type Greeting = `hello ${string}`` accept, and how is it different from plain `string`?
basics
~20 sAny string that begins with "hello " — the placeholder matches any string, so "hello world" is assignable but "hi" is not. The type is a narrower subtype of string, and the match is checked only at compile time.
In TypeScript, given `type Shade = 'light' | 'dark'` and `type Color = 'red' | 'blue'`, what type is `` `${Shade}-${Color}` `` and what rule produces it?
basics
~10 sIt is the four-member union "light-red" | "light-blue" | "dark-red" | "dark-blue". A union placed in a placeholder distributes, so the result is the cross-product of every placeholder's members.
In TypeScript, what does `type Tail<S> = S extends `${infer Head}-${infer Rest}` ? Rest : never` give for `Tail<'a-b-c'>`, and what matching rule explains it?
basics
~10 sIt gives "b-c". A placeholder followed by literal text matches as little as possible, so Head captures "a" at the first hyphen and Rest absorbs everything after it, separators included.
A codebase types its navigation helper as `function navigate(path: `/users/${string}`): void`. What does that annotation actually guarantee, and where does it stop helping?
basics
~20 sIt only constrains call sites whose argument still carries a literal or template literal type. A value typed plain string is rejected until someone asserts it, and nothing is verified at runtime because the type is erased.
Which four string-manipulation types does the TypeScript compiler provide as built-ins, what does each do, and why can you not implement them yourself in the type system?
basics
~10 sUppercase, Lowercase, Capitalize and Uncapitalize. Each transforms a string literal type, and they are declared as intrinsic in lib.es5.d.ts, meaning the compiler implements them natively because the type system has no character-level string operation.
In TypeScript, how do you write a single type that describes any value `JSON.parse` can return, including arbitrarily deep nesting, and why is a type alias allowed to name itself?
basics
~20 sUse a recursive type alias: a JSON value is a string, number, boolean, null, an array of JSON values, or an object whose values are JSON values. The self-reference is legal because TypeScript resolves it lazily instead of expanding it eagerly.
A codebase defines `type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] }` and applies it to a type with `string[]`, `Date`, and callback properties. What goes wrong, and how do you harden it?
basics
~20 sThe extends object test also matches arrays, Dates, Maps and functions, so the helper recurses into them: array elements gain undefined, a Date becomes an object of optional methods, and a function loses its call signature. Fix it by bailing out on those shapes before the object branch.
How would you write a recursive type that produces the union of dotted key paths ("user.address.city") for a nested object type, and what breaks when the object contains arrays or a self-referencing node?
basics
~20 sRecurse over the keys, emitting each key and, for object-valued keys, that key joined to the child's paths with a dot. Arrays leak Array's own member names into the union, and a self-referencing type recurses forever until the compiler reports an excessive-depth error.
What does TypeScript's error "Type instantiation is excessively deep and possibly infinite" mean, and what do you change in a recursive conditional type to get past it?
basics
~20 sThe checker hit its cap on nested type instantiations while expanding a recursive type and gave up rather than hang. Fix it by adding or correcting a base case, rewriting the recursion with an accumulator so the recursive call sits in tail position, or capping depth with a counter.
In TypeScript, why can type-only helpers slow `tsc` and editor IntelliSense down noticeably, and which kinds of types tend to be the expensive ones?
basics
~20 sThe checker does real work per generic instantiation and per assignability comparison, and the editor's TypeScript server repeats it while you type. Large unions, deeply nested conditionals, long intersection chains and inferred-instead-of-annotated return types are the usual expensive shapes.
Do elaborate TypeScript types — deeply nested conditional and mapped helpers — make the shipped JavaScript slower, and if not, where does their cost actually land?
basics
~20 sNo — TypeScript erases types, so even elaborate helpers emit no JavaScript and cost nothing at run time. Their price is paid at check time: slower tsc runs, laggy editor completions, and error messages teammates struggle to read.
A generic TypeScript helper produces an error message that runs for pages and is cut off with `...`. Why do computed types fail this way, and how do you get the errors under control?
basics
~20 sThe checker reports the failure with both sides fully expanded at the outermost place it noticed, so a computed type prints as its whole structure and gets truncated. Control it by naming intermediate aliases, constraining parameters, testing types in isolation, and using --noErrorTruncation when you need the full text.
A TypeScript project's `tsc --noEmit` has crept from twenty seconds to four minutes with no new dependencies. How do you find which types are responsible?
basics
~20 sMeasure before rewriting anything. Run tsc with --extendedDiagnostics to see whether check time and instantiation counts dominate, then --generateTrace into a directory and analyse the trace with @typescript/analyze-trace to get the specific files and positions that are slow.
As the lead on a shared TypeScript codebase, how do you decide how much type-level complexity to allow, and when should the guarantee move to a runtime validator such as Zod instead?
basics
~20 sJudge each clever type by guarantee per unit of cost: build and editor latency, plus how many teammates can maintain it. Anything about data entering the program from outside cannot be guaranteed by types at all and belongs in a runtime validator, with the static types derived from that schema.