Concept
The beginner framing: TypeScript ships a set of built-in utility types, Partial, Pick, Omit, Record, ReturnType, and others, that transform an existing type into a new one, covering extremely common shape-transformation needs without writing custom mapped/conditional types by hand every time.
interface User { id: number; name: string; email: string }
type PartialUser = Partial<User>; // { id?: number; name?: string; email?: string }
type UserName = Pick<User, "name">; // { name: string }
type UserNoEmail = Omit<User, "email">; // { id: number; name: string }
type Scores = Record<"math" | "science", number>; // { math: number; science: number }These aren't special compiler magic, they're ordinary generic types, defined in TypeScript's own standard library type definitions, built from the same mapped-type and conditional-type mechanisms available to any code. Seeing how each is actually implemented demystifies them completely.
Hand-implementing each one
type MyPartial<T> = { [K in keyof T]?: T[K] };
// for every key K in T, make it optional, keeping the same value type
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
// build a new type using only the keys in K, taken from T
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>;
// Omit is just Pick, given the COMPLEMENT of the keys to remove
Confirmed by compiling all four against real usage (MyPartial<User>, MyPick<User, "name">, etc.), each hand-rolled version behaves identically to its built-in counterpart. MyOmit's definition in particular reveals something non-obvious: Omit isn't a separate primitive mechanism, it's just Pick combined with Exclude (covered in Advanced Types) to compute "all the keys except these," then picking those.
NoInfer<T>, stable since TypeScript 5.4
// WITHOUT NoInfer: BOTH parameters contribute to inferring T
function createStateBad<T>(initial: T, options: { validValues: T[] }): T {
return initial;
}
const bad = createStateBad("a", { validValues: ["a", "b", "c"] });
// T infers as `string`, WIDENED, because validValues' array contributes too
// WITH NoInfer: block `validValues` from influencing inference
function createStateGood<T>(initial: T, options: { validValues: NoInfer<T>[] }): T {
Confirmed by compiling both: without NoInfer, TypeScript infers T by combining candidates from every parameter position where T appears, here, both initial ("a") and validValues (an array widening to string) contribute, and the result widens to plain string. Wrapping the second parameter's type in NoInfer<T> excludes that position from contributing to inference at all, T is inferred purely from initial, staying locked to the literal "a". As a genuinely useful side effect, validValues's elements are then checked against that locked-in T rather than helping determine it, so would now correctly flag as invalid, since it doesn't match the type already locked in by .
Try It
Predict the outcome before checking the solution.
function setDefault<T>(value: T, fallback: NoInfer<T>): T {
return value ?? fallback;
}
setDefault(5, "not a number"); // does this compile?Solution
No, this fails to compile. T is inferred purely from value (5, so T is number), since fallback's type is wrapped in NoInfer<T> and therefore excluded from contributing to inference. But fallback is still checked against the already-inferred T, and "not a number" isn't assignable to number, producing a compile error. This is exactly the intended use case for NoInfer: ensuring a "fallback" or "default" argument must match the type of the "primary" argument, rather than letting a mismatched fallback silently widen the inferred type to accommodate both.
Implement It Yourself
Build a DeepPartial<T> utility, recursively applying Partial to nested objects, something the built-in Partial deliberately does NOT do:
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Config {
server: { port: number; host: string };
debug: boolean;
}
// built-in Partial only makes the TOP level optional:
type ShallowPartial = Partial<Config>;
// { server?: { port: number; host: string }; debug?: boolean }, server's INNER fields still required
// DeepPartial recurses into nested objects too:
This demonstrates both how conditional types (T extends object ? ... : T) combine with mapped types to build genuinely new utility types, and a real limitation of the built-in Partial worth knowing, it's intentionally shallow, not recursive.
Under the Hood
Every hand-implementation here relies on mapped types ({ [K in keyof T]: ... }) and conditional types (T extends U ? X : Y), both covered in full depth in Advanced Types, this topic applies those mechanisms to a specific, extremely common set of practical use cases, but the underlying machinery is exactly what's covered there. And MyReturnType's use of infer previews the mechanism infer, Variance & Declaration Merging covers in full generality.
Common Mistakes
1. Assuming Partial<T> is recursive
interface Config { server: { port: number } }
const c: Partial<Config> = { server: { } }; // ❌ still an error, server.port is still REQUIREDPartial only makes the type's own top-level properties optional, nested object properties are unaffected. A recursive DeepPartial (shown above) is needed for that, and isn't built in.
2. Not realizing Omit is implemented in terms of Pick
type UserNoEmail = Omit<User, "email">; // conceptually: Pick<User, all keys EXCEPT "email">Understanding this relationship (rather than treating Omit as an unrelated, separate mechanism) makes it much easier to reason about what Omit actually produces, especially when combined with other utility types.
3. Reaching for NoInfer when the real fix is a different function signature
function setDefault<T>(value: T, fallback: NoInfer<T>): T { ... } // fine, for THIS shapeNoInfer solves a specific inference problem (one parameter unintentionally widening T), it's not a general-purpose fix for every generic inference confusion; sometimes restructuring the function's parameters or splitting it into two overloads is the more appropriate solution.
Best Practices
- Reach for the built-in utility types first (
Partial,Pick,Omit,Record,ReturnType, and others) before hand-rolling an equivalent, they're well-tested, well-understood by anyone reading the code, and cover the vast majority of real needs. - Know when a built-in isn't sufficient,
Partial's shallowness being the most common surprise, rather than assuming it, so a real bug isn't traced back to a subtly wrong assumption about a utility type's exact behavior. - Use
NoInferspecifically for "fallback/default value" style parameters that should be checked against, but not influence, the type inferred from a primary argument.
Performance Tips
- Utility types, like all TypeScript types, are fully erased at compile time, using
Partial<User>versus manually writing out the equivalent optional-property object type has zero runtime difference; the choice is purely about maintainability and clarity. - Deeply recursive custom utility types (like
DeepPartialapplied to a very deeply nested type) can noticeably slow downtsc's type-checking on large, complex types, a real build-time consideration for very large codebases, though never a runtime one.
