Community TypeScript Snippet
The Five Custom TS Utility Types I Add to Every Project
TypeScript ships great utility types but the five I miss in every fresh project are these: NonEmptyArray, Branded, Awaited inverse (Promisify), DeepReadonly, and ExactKeys. Each is one or two lines that has saved me from a bug.
The Five Custom TS Utility Types I Add to Every Project
TypeScript ships great utility types but the five I miss in every fresh project are these: NonEmptyArray, Branded, Awaited inverse (Promisify), DeepReadonly, and ExactKeys. Each is one or two lines that has saved me from a bug.
By @tylerperry
March 21, 2026
·
Updated May 20, 2026
834 views
14
4.3 (14)
NonEmptyArray is the tiny win that prevents the most common TypeScript bug I see in code review: indexing an array and getting T | undefined. By declaring at the type level that the array has at least one element, downstream code can use arr[0] without a guard. Branded<T, B> solves the opposite-shape problem: the compiler treats string as a universal currency for ids, but in practice mixing up a UserId and an OrgId is exactly the kind of bug that ships to production. The phantom __brand field is erased at runtime, so the cost is zero, and the compiler treats UserId and OrgId as incompatible.
DeepReadonly is the type I want every time I load a config or a feature-flag map. The conditional type checks for the four collection shapes I care about (array, Map, Set, plain object) before falling through to the leaf case. Pairing it with a runtime freezeDeep walker gives both compile-time and runtime safety; without the runtime side a type assertion can bypass the check, and without the type side an editor will not warn you about a forbidden mutation. I have caught at least three race conditions where one module mutated config.flags.betaCohorts.push('foo') and a later module saw the side effect; the type alone would have flagged the push at compile time.
Promisify is the type I write when migrating a sync API to async without breaking call sites. The conditional type infers the function's argument tuple A and return type R, then rebuilds the signature with Promise<Awaited<R>> so a function that already returned a Promise does not get wrapped twice. The Awaited is what handles the double-promise case (Promise<Promise<T>> collapses to Promise<T>). Pairing it with a typed event emitter where Listener<T> accepts either sync or async handlers is the production pattern that uses these types together. The r && r.then check at runtime is needed because TypeScript cannot statically tell which branch a listener took.
ExactKeys is the type I add to every public API surface where call-site typos would silently be ignored. The trick is the intersection: Record<Exclude<keyof U, keyof T>, never> says "any key in U that is not in T must have type never". Since never is uninhabited, providing a real value triggers a type error. The reason this matters is that TypeScript's normal subtyping happily accepts extra fields, so a typo like tiemout: 3000 simply slips through to runtime where it is silently discarded. Using ExactKeys at the boundary catches those at the call site without affecting the implementation, which still consumes the standard RequestOptions shape.
