Topic 10.3
Building Your Own Utilities
In one line
When the built-ins aren't enough you write your own: DeepPartial, Mutable, PickByValue and RequireAtLeastOne. The method is the same every time: start tiny, ask // ^? after every step, and fix what the answer shows.
Think of it like this
Building with LEGO from a picture on the box. You don't snap 200 pieces together and hope: you build one small part, look at it, compare it with the picture, then add the next part. **A type-level utility is built the same way: one small step, one // ^?, then the next step.** If you build everything at once and the result is wrong, you can't tell which piece is wrong.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Generic type alias
- A type with a type parameter, like
type Box<T> = { value: T }. - Recursion
- A type that refers to itself, so it can go as deep as the data goes.
- Key remapping (`as`)
- A clause inside a mapped type that renames a key, or drops it when it gives
never(TypeScript 4.1). - Filtering
- Dropping keys by remapping them to
never. - Intersection `&`
- A type that has all the properties of both sides.
- Indexed access `T[K]`
- The type of property
KofT. With a union of keys it gives a union of property types. - Identity mapped type
{ [K in keyof T]: T[K] }: the same type rebuilt, often used to make hover text readable.- Debugging a type
- Splitting it into named steps and asking
// ^?for each.
Step by step
01Start with the smallest one: Mutable
Mutable<T> is a mapped type that copies each property and removes readonly. Query it on a nested config and look at the result.
The top-level server and debug lose readonly, but inside server, host and port keep theirs. That is what "shallow" means.
02Go deep with recursion
To reach nested objects, call the same type on each property: DeepPartial<T[K]>. Add the two guards: a function is returned unchanged, and anything that is not an object is returned unchanged.
Now a patch like { server: { port: 8080 } } is allowed, and { server: { port: "8080" } } is still an error. The code in the example merges the patch into a base config.
type DeepPartial<T> = T extends (...args: any) => any
? T
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;03Filter keys with as
PickByValue<Product, number> walks the keys and keeps one only when its value type extends number. The others are mapped to never and vanish.
04Build a union of shapes
RequireAtLeastOne<Contact, "email" | "phone"> builds one version of Contact per key, with that key required. The union means "email is present, or phone is present, or both".
An object with only name fits none of the versions. The error names one of them (here the phone version) and says that phone is missing.
05Debug by taking it apart
Suppose you want "the keys of Row whose value is a string". The first try gives "name" | undefined: the undefined leaks in from the optional note.
Query the middle step (the mapped type before the [keyof T] lookup), see note?: never, and add -? to remove the optionality. Now it is "name".
Try it yourself
- 1
Filter the other way
In the second example write
OmitByValue<T, V>by swappingKandneverin theasclause. PredictOmitByValue<Product, number>and check it. - 2
Make a nested readonly
Write
DeepReadonly<T>with the same shape asDeepPartialbut withreadonly [K in keyof T]: DeepReadonly<T[K]>. Then tryinterface Plain { server: { port: number } },declare const shallow: Readonly<Plain>;anddeclare const deep: DeepReadonly<Plain>;. Writingshallow.server.port = 1;is accepted (shallow), butdeep.server.port = 1;giveserror TS2540: Cannot assign to 'port' because it is a read-only property.(Do not useConfigfrom the first example here: its properties are alreadyreadonly, so the error would appear either way.) - 3
Three required keys
Add
fax?: stringtoContactand useRequireAtLeastOne<Contact, "email" | "phone" | "fax">. Which assignments are legal now? Any one, two or all three of the keys is fine. For{ name: "Zed" }the error ends withProperty 'phone' is missing in type '{ name: string; }' but required in type 'Required<Pick<Contact, "phone">>'.
Code & diagrams
Mutable is shallow: server is writable but its host and port keep readonly. DeepPartial reaches the inside, so the patch may set only port, and a text port is still an error.
Console
(nothing was printed)Expected console output
{ server: { host: 'localhost', port: 8080 }, debug: false }Keys whose value does not extend V are remapped to never and dropped. Change V to boolean and predict the result.
Console
(nothing was printed)Expected console output
{ id: 1, price: 99 }The last assignment is rejected because neither email nor phone is present. The error text shows one of the generated versions of the type.
Console
(nothing was printed)Expected console output
{ name: 'Asha', email: 'a@x.in' } { name: 'Ravi', phone: '99999' } { name: 'Mia', email: 'm@x.in', phone: '11111' }Step shows the middle result. The optional note keeps its ? and so contributes undefined when you index with [keyof T]. -? removes it.
Console
(nothing was printed)Expected console output
nameBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Let undefined leak in
In the last example, ask Wrong<Row> and compare it with Right<Row>.
Break #2
A text where DeepPartial expects a number
In the first example call update(base, { server: { port: "8080" } });.
Myth vs fact
Myth
Custom utility types need advanced tricks nobody can learn.
Fact
They are made of the same few pieces: keyof, T[K], mapped types, as, conditionals, infer. Small steps plus // ^? is the whole method.
Myth
DeepPartial works the same as Partial for arrays and functions.
Fact
A naive version breaks them: it rebuilds a function as an object. That is why the version above returns functions unchanged. Arrays and tuples are mapped element by element by homomorphic mapped types.
Myth
If a utility compiles, it's right.
Fact
A utility can compile and give the wrong answer, like "name" | undefined. Always query a few small examples.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Mapped types over a type parameter that is an array or tuple map each element and keep the array shape, so
DeepPartial<string[]>gives(string | undefined)[], not an object with array keys. Union inputs distribute because the outer conditional checks a nakedT. - ▸
Intersections like
Pick<...> & { ... }[Keys]print badly in hovers. Wrap the result in an identity mapped type ({ [K in keyof X]: X[K] }) to flatten it for display. It also changes error text, which tells you the display is separate from the meaning. - ▸
Recursive utilities have a cost: each level is a new type to instantiate and cache. Deep object types or long unions can slow the editor. Topic 10.6 covers limits (
Type instantiation is excessively deep) and Topic 12.6 shows how to measure.
Remember this
- 1
The problem this solves: The built-in utilities don't cover every need, and a big custom type written all at once is hard to debug. Building it in small checked steps keeps it understandable. A custom utility is a generic type alias (
type Name<T> = ...) made from the pieces you already know:keyof, indexed accessT[K], mapped types, key remapping withas(TypeScript 4.1, Topic 8.5), conditional types andinfer. Nothing new is needed. What is new is the habit: write it small, test it on a tiny example type, and look. - 2
Mutable<T>is the simplest:{ -readonly [K in keyof T]: T[K] }. The minus beforereadonlyremoves the modifier, so it undoesReadonly. It is shallow, just likeReadonly, so nested objects keep their ownreadonlymarks (you'll see that in the example). - 3
DeepPartial<T>needs recursion (a type that uses itself): for each property, applyDeepPartialagain to that property's type. Two guards matter. Functions must be returned as they are (otherwise the mapped type would rebuild them as plain objects), and primitives must be returned as they are (a mapped type overstringis not what you want). So the shape is: "if it's a function, keep it; else if it's an object, map over it; else keep it". - 4
PickByValue<T, V>keeps only the properties whose value type fitsV. It uses key remapping:[K in keyof T as T[K] extends V ? K : never]. When theasclause givesneverfor a key, that key is dropped. This is **filtering withas**, the pattern behind many practical utilities. - 5
RequireAtLeastOne<T, Keys>says "at least one of these optional fields must be present" (a contact needs an email or a phone). For each key inKeysit builds a version where that key is required and the others stay optional, then takes the union of those versions. The object must match at least one of them. It is the first utility where the result is a union of object types, not one object type. - 6
When a utility gives the wrong answer, take it apart. Copy the inside of the mapped type into a separate alias and query it. A common surprise is optional properties:
{ [K in keyof T]: ... }[keyof T]addsundefinedto the result whenThas an optional property, because indexing an optional property includesundefined. The fix is-?in the mapped type (the last example shows this with real output).
Explain it without notes
Walk through writing PickByValue<T, V>.
Why does a naive DeepPartial break functions, and how do you guard it?
How would you debug a utility that returns the wrong type?
How does RequireAtLeastOne work?
Practice
Write OptionalKeys<T> that gives a union of the keys of T that are optional. Test it on Row from the last example.
Write Getters<T> that turns { name: string; age: number } into { getName: () => string; getAge: () => number }.
Trade-offs
- ↔
A custom utility removes repetition across many types, but it also adds something every reader must learn. If a utility is used once, write the type out by hand.
- ↔
Name and test your utilities. A one-line comment with an example (
// RequireAtLeastOne<Contact, "email" | "phone">) saves the next reader an hour.
Done when you can
I can write
Mutable,DeepPartialandPickByValuefrom scratch.I can filter keys with an
asclause that givesnever.I can explain how
RequireAtLeastOnebuilds a union of shapes.I can debug a utility by splitting it into steps and querying each with
// ^?.I know that
-?stopsundefinedleaking out of{ ... }[keyof T].