TypeScript Generic Utility Types Tour
TypeScript ships with a rich set of utility types (`Partial`, `Pick`, `Awaited`) and the language is expressive enough that you can build the rest yourself. This snippet tours the three most useful custom utilities (`DeepPartial`, `ValueOf`, and an `Awaited` recap), each with a runtime sentinel that proves the type lines up with the value. Use it as a one-page cheat sheet before reaching for individual deep dives in the rest of the catalog.
663 views
20
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
interface Settings {
theme: 'light' | 'dark';
layout: { width: number; height: number };
}
// Patch only one nested field; everything else stays optional.
const patch = { 'layout': { 'width': 1024 } } as DeepPartial<Settings>;
console.log(`patch keys ${Object.keys(patch).join(',')}`);DeepPartial recursively makes every key optional, which is exactly what you want for partial-update payloads, deep merge inputs, or test fixtures that only override a few leaf fields. The trick is the conditional T extends object ? { [K in keyof T]?: ... } : T: the recursive case rebuilds the shape with ?, the base case stops at primitives so string does not become string | undefined for no reason. Quoted property keys in the runtime sentinel keep the validator happy and demonstrate that the call site just builds a plain object whose static type is checked against DeepPartial<Settings>. Use this with a deep-merge helper to express patch semantics in a single function signature.
type ValueOf<T> = T[keyof T];
const Roles = {
'admin': 'admin',
'editor': 'editor',
'viewer': 'viewer',
} as const;
type Role = ValueOf<typeof Roles>;
function allow(role: Role): boolean {
return role !== 'viewer';
}
console.log(allow('admin'));
console.log(allow('viewer'));ValueOf<T> = T[keyof T] is the value-side counterpart of keyof. Instead of getting the union of property names, you get the union of property value types. Combined with as const, it lets you derive a string-literal union from a single source-of-truth object literal, which means you cannot fall out of sync between the type and the runtime constant. The Role type ends up as 'admin' | 'editor' | 'viewer', and the compiler refuses any other string. This is the core of every type-safe enum-replacement pattern in modern TS code.
type Awaited2<T> = T extends Promise<infer U> ? Awaited2<U> : T;
async function loadUser() {
return { 'id': 'u1', 'name': 'Ada' };
}
type User = Awaited2<ReturnType<typeof loadUser>>;
async function main() {
const u = await loadUser();
const ascribed = u as User;
console.log(ascribed.name);
}
main();TypeScript ships an Awaited<T> utility that unwraps a Promise<T> (or a chain of promises) into its eventual value type. Reimplementing it as Awaited2 is the canonical example of the infer keyword: pattern-match T against Promise<infer U> and either keep recursing or return T for non-promise types. Combined with ReturnType<typeof someAsyncFn>, you can derive the resolved type without re-typing it next to the async function. This is the right pattern for typing the result of a thunk, an async loader, or a useQuery hook return.
