Skip to content

Typescript Generics Patterns And Constraints

241 companion flashcards · AI-assisted study content · Open the deck →

This deck is a focused review of TypeScript's generics, from the basics of writing a generic function signature to more advanced patterns like conditional types, mapped types, and built-in utility types such as Partial, Required, Readonly, Pick, and Omit. The cards walk through constraining generics with extends, supplying and defaulting type arguments, and pulling types out of arrays and functions. Together, they cover the kind of type-level toolkit you'll reach for when shaping reusable, well-typed APIs.

It's well suited for developers who already feel comfortable with everyday TypeScript syntax and want to level up their ability to write expressive, reusable type definitions. If you've ever copied a Partial<T> or utility type from Stack Overflow without quite understanding how it works, these cards will help demystify the patterns and let you build your own. You'll also find it handy as a reference when designing generic functions, libraries, or component prop types.

To get the most out of the deck, treat each card as a tiny coding exercise: try writing the type out from memory before flipping, and where possible, drop it into a real editor with a few sample inputs to see how it behaves. Because generic patterns build on each other, revisiting the earlier cards after working through the more advanced ones will reinforce how far your understanding has come. Spacing your review across a few short sessions tends to work better than cramming everything at once, especially once the conditional and mapped type cards start layering up.

Foundations of Generics

A generic in TypeScript is a placeholder for a type that is filled in at the call site, letting a single function, class, or interface operate over many types while preserving the relationships between inputs and outputs. The simplest form is a generic function such as function identity<T>(x: T): T { return x; }, where T is a type parameter that TypeScript infers from the argument; calling identity('hi') causes T to be inferred as string, while identity<number>(42) provides it explicitly. Unlike any, generics keep static type information flowing through the call, so a single helper stays type-safe across arbitrary inputs rather than opting out of checking.

The power of generics comes from constraints. Using T extends SomeType narrows the set of types that satisfy T, allowing operations that depend on those properties inside the body. For example, function len<T extends { length: number }>(x: T): number { return x.length; } accepts strings, arrays, and any custom type that exposes a numeric length because they all match the constraint; a function like function f<T>(): T {}, on the other hand, fails because T is unconstrained and the body cannot derive a concrete value to return. Type parameters can be made dependent on each other: K extends keyof T is the canonical pattern for safe property access, while a constraint chain like <T extends object, K extends keyof T, V extends T[K]> lets later parameters depend on earlier ones. Default type parameters such as K extends keyof T = keyof T give callers a sensible fallback when they don't supply a value, mirroring the type State<T = unknown> idiom.

Generic syntax extends naturally to interfaces, type aliases, and classes. interface Box<T> { value: T; }, type Box<T> = { value: T }, and class Box<T> { constructor(public value: T) {} } all behave identically for object shapes, while a class can also expose its own per-method type parameters, like class A { id<T>(x: T): T { return x; } }. A useful F-bound—<T extends Comparable<T>>—lets a method return the same narrowed subtype instead of the base, supporting self-typed comparisons such as compareTo(other: T). A practical design heuristic is to start from the way you want callers to use the generic and then build the signature to satisfy that site; this pushes you toward tightly typed, helpful APIs rather than toward any-shaped escape hatches.

Built-in Utility Types and Modifiers

TypeScript ships a rich set of utility types that compose new shapes from existing ones. The companions of Partial<T> are Required<T>—implemented as { [K in keyof T]-?: T[K] }—and Readonly<T>, each applying a single modifier to every property. To go deeper, a hand-rolled DeepReadonly<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> } recurses into nested objects, while a custom Mutable<T> = { -readonly [K in keyof T]: Mutable<T[K]> } strips read-only-ness recursively. The negation modifiers -readonly and -? are the standard way to flip mapped-type flags, and they're the building blocks of every Required-style helper you might want to write yourself.

For selecting and reshaping keys, Pick<T, K> keeps a subset of properties and Omit<T, K> removes them, often expressed as Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>. Record<K, V> builds an object whose keys are taken from a union of string or number literals with a uniform value type, so Record<'a' | 'b', number> becomes { a: number; b: number }. For union manipulation, NonNullable<T> removes null and undefined, Exclude<T, U> removes members assignable to U, and Extract<T, U> keeps only those assignable to U; together these are the LEGO bricks for almost every larger mapped type.

The library also exposes function-shape utilities: ReturnType<T>, Parameters<T>, ConstructorParameters<T>, InstanceType<T>, and ThisParameterType<T> / OmitThisParameter<T> all operate on function or constructor types, extracting the return, parameter tuple, or instance type without restating the signature by hand. Awaited<T> recursively unwraps Promise (and thenable) chains, returning the deeply resolved value. Finally, an aesthetic trick: type Pretty<T> = { [K in keyof T]: T[K] } & {} forces TypeScript to fully evaluate and flatten intersections in IDE tooltips, so complex mapped outputs read as plain objects rather than as an opaque A & B. And although readonly T[] and ReadonlyArray<T> are largely equivalent for inference, modern code prefers readonly T[] in function parameter positions to advertise non-mutation.

Conditional Types and Inference

A conditional type has the shape T extends U ? A : B and is evaluated at the type level: if the source type is assignable to the target, you get A; otherwise B. The most consequential behavior is distribution: a naked type parameter on the left of extends distributes the conditional across every member of a union, so (string | number) extends any ? F<T> : never becomes F<string> | F<number>. The same mechanism explains why T extends T ? F<T> : never is a useful trick—because the left T distributes, you get F applied to each member of T. To opt out, wrap the variable in a tuple, so [T] extends [U] ? A : B checks whole-type assignability without spreading.

The infer keyword lets you bind a type variable inside a conditional, which is how most of the standard utilities are implemented. ReturnType<T> is essentially T extends (...args: any) => infer R ? R : never, while Parameters<T> captures the parameter tuple via (...args: infer P) => any ? P : never. The same pattern recursively unwraps promises: Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T traverses Promise<Promise<Promise<string>>> down to string, and the standard library's Awaited<T> is the same idea generalized to thenables. Other recipes include extracting array elements with T extends (infer U)[] ? U : never, flattening a union of arrays into one union, capturing the first parameter of a function via F extends (a: infer A, ...r: any[]) => any ? A : never, and even converting a union to an intersection using the contravariance trick (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never.

Returning never from a conditional is a common filtering idiom, since never as a branch result drops the offending member. Conditional return types scale better than overloads when there are many cases or when the result type depends on the parameter literally: function get<T extends 'a' | 'b'>(t: T): T extends 'a' ? number : string returns number for 'a' and string for 'b', with full inference preserved. A subtle behavior worth knowing is T extends infer U ? ... : never, which introduces a fresh inference variable that can sometimes nudge the inference algorithm the right way. Finally, TypeScript enforces a recursion depth of roughly 50 instantiations, so deeply recursive types should be written with tail recursion—type Reverse<T extends any[], R extends any[] = []> = T extends [infer H, ...infer Rest] ? Reverse<Rest, [H, ...R]> : R—or split into multiple, smaller aliases.

Mapped Types and Template Literal Types

A mapped type walks the keys of an existing type and produces a new one, in the form { [K in keyof T]: ... }. The built-ins Partial<T>, Required<T>, and Readonly<T> are all mapped types, optionally combined with the ? or readonly modifier (or its negation -? / -readonly). Key remapping with as in the form [K in keyof T as NewKey]: T[K] lets you rename, filter, or drop keys; using never as the new key deletes the entry, which is why T[K] extends Function ? K : never keeps only function-valued members and T[K] extends number ? never : K removes numeric ones. The same idiom appears in patterns like type FunctionsOf<T> = { [K in keyof T as T[K] extends Function ? K : never]: T[K] } and helper extraction like type FnNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T].

Mapped types and template literal types combine to give precise, declarative key transforms. The string helpers Uppercase, Lowercase, Capitalize, and Uncapitalize operate on literal types, so { [K in keyof T as Uppercase<string & K>]: T[K] } produces an uppercase key set, while a getter-style remap as `get${Capitalize<string & K>}` turns each property name into a corresponding accessor. Filtering and renaming can also use template inference—for instance, dropping a leading __ with K extends `__\({infer R}` ? R : K, or prefixing every key via as `__\){string & K}`. A union of templates yields the cartesian product of strings, and type Greeting = `hello \({'world' | 'there'}` expands directly to the union 'hello world' | 'hello there'; combining `\){'get' | 'set'}${Capitalize<string>}` produces 2N string literals at compile time.

Because every mapping can recurse, mapped types express deep transformations in a few lines: DeepPartial<T>, DeepReadonly<T>, and Mutable<T> all walk into nested objects automatically, while a focused mapped type like type Paths<T> = { [K in keyof T & (string | number)]: T[K] extends object ? `\({K}` | `\){K}.\({Paths<T[K]>}` : `\){K}` }[keyof T & (string | number)] computes dotted paths through a structure. The as clause combined with conditional filters also serves as lightweight key engineering: Rename<T, From, To>, Omit equivalents via remap, and union-or-rename operations can all be expressed without writing bespoke helpers. When tooltips collapse into an unreadable intersection, the Pretty<T> trick { [K in keyof T]: T[K] } & {} forces TypeScript to display the flattened object form, making complex mapped results inspectable in editors. Note that keyof on an indexed type yields string | number | symbol because keyof any does; this matters when mixing named properties with an index signature, since any named property must still satisfy the indexer's value type.

Narrowing, Type Guards, and Branded Types

TypeScript narrows a value's type within control-flow branches using predicates and operators. A user-defined type guard has the form function isString(x: unknown): x is string { return typeof x === 'string'; }, and after if (isString(x)) the variable x is known to be a string. An assertion function—function assertString(x: unknown): asserts x is string—narrows afterwards rather than inside a branch, while the simpler function assert(x: unknown, msg?: string): asserts x only asserts that the value is truthy without committing to a particular type. The distinction matters: a predicate returns a boolean and narrows inside the calling branch, an assertion narrows for the rest of the calling scope.

Built-in narrowing follows the same shape. typeof handles primitives ('string', 'number', 'boolean', 'symbol', 'undefined', 'object', 'function', 'bigint'), instanceof narrows to a class instance, and 'foo' in obj narrows to types that include that property. When the discriminant is a literal-typed field such as kind: 'circle' | 'square', a switch on kind peels off each variant of the discriminated union. To force every case to be handled, a default branch assigns the parameter to a never: const _exhaustive: never = shape;—if a new variant is added later, the assignment errors. Wrapping this in function assertNever(x: never): never { throw new Error('unexpected'); } produces a reusable helper that doubles as a runtime safety net, and type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E } is a famous generic discriminated union that pattern-matches cleanly through this mechanism.

Branches propagate narrowing into compound expressions and array methods. After if (typeof x === 'string'), the else branch sees the negation; inside arr.filter((x): x is NonNull => x != null), the array's element type narrows in subsequent chained calls; and custom guards like a is [] and a is [T, ...T[]] let you distinguish empty from non-empty arrays in user code. Branded (or nominal) types simulate stronger typing on top of structural types: type UserId = string & { __brand: 'UserId' } produces a type that is structurally still a string, but only assignable through a factory function, preventing accidentally passing a raw string where a UserId is expected. A more opaque variant uses a unique symbol tag declared inside a .d.ts file: declare const tag: unique symbol; type UserId = string & { [tag]: 'UserId' };—because unique symbol types are file-local singletons, the brand cannot be forged elsewhere.

Tuple and Variadic Generics

Tuples let generic types represent fixed-shape arrays precisely, and TypeScript extends this idea with variadic tuple types—tuples containing rest elements like [string, ...number[], boolean]. The simplest operations use rest inference: type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never and type Last<T extends any[]> = T extends [...any[], infer L] ? L : never pluck the edges, while type Len<T extends any[]> = T['length'] reads the tuple's length. T[number] turns a tuple into a union of its element types, providing the canonical "tuple-to-union" path; the reverse, union-to-tuple, requires recursive conditionals and is generally avoided because it can quickly hit TypeScript's instantiation depth limit.

Variadic generics bind a function's argument list to a tuple parameter, preserving types end-to-end: function fn<T extends any[]>(...args: T): T records the exact types passed in. From there, type-level Concat<A extends any[], B extends any[]> = [...A, ...B], Reverse<T extends any[]> = T extends [infer H, ...infer R] ? [...Reverse<R>, H] : [] (or the safer tail-recursive Reverse<T, R extends any[] = []>), and ToAsync<F> = F extends (...a: infer A) => infer R ? (...a: A) => Promise<R> : never provide composable pieces, and a Curry<F> type decomposes a function signature into nested single-argument functions, recursing until only the return value remains. Sync<F> is the mirror of ToAsync, stripping a Promise layer from the return type when present and otherwise leaving the signature unchanged.

These ingredients shine in API design. function head<T extends readonly any[]>(arr: T): T[number] | undefined infers the exact element type from a readonly array; type NonEmpty<T extends any[]> = T['length'] extends 0 ? never : T flags empty tuples via the literal length; and tuple-typed guards like a is [] and a is [T, ...T[]] distinguish empty from non-empty arrays. Real-world typings often mirror this pattern, and type-safe Object.keys, Object.entries, and Object.fromEntries are notoriously awkward in TypeScript because TS can't guarantee the exact key set when subclasses are involved—function keys<T extends object>(o: T): (keyof T)[] still has to be cast at runtime, but at least the declared return type matches reality. As a rule of thumb, prefer tuple inference ((...args: T) => T) when you need to preserve argument identity, and use a single named tuple (e.g., [string, ...number[]]) when argument shape is itself meaningful.

Advanced Patterns, Ecosystem, and Tooling

Several language features elevate generics from "type-safe containers" into full API design tools. The satisfies operator validates that a value conforms to a shape without forcing it to that shape, so const config = { a: 1 } satisfies Record<string, number> keeps the literal 1 in the inferred type of config while still rejecting wrong shapes at compile time. This pairs naturally with as const: const config = { a: 1 } as const satisfies Schema first freezes literals and then validates conformance. Combined with const type parameters—function fn<const T>(x: T): T, available in TS 5.0+—literal types can be preserved through ordinary calls without an explicit as const at every site, although for values built piecewise through intermediate variables, as const is still needed because TypeScript's widening tracker can't follow every step. The companion tool NoInfer<T> (TS 5.4+) blocks inference from a particular position, so helpers like fn<T>(a: T, b: NoInfer<T>) infer T only from the first argument.

The ecosystem around these features is substantial. type-fest provides zero-runtime helpers such as Promisable, RequireExactlyOne, SetOptional, SetRequired, ConditionalKeys, and JsonValue; ts-toolbelt is a heavier type-level utility library covering everything from Object.Diff to List.Concat. For runtime validation, z.object(...).infer<typeof Schema> and the io-ts codec pattern tie a runtime schema directly to a static type, addressing the well-known pitfall that JSON.parse returns any by default. Builders and mixins use generics to accumulate type information: class B<T = {}> { set<K extends string, V>(k: K, v: V): B<T & { [P in K]: V }> { ... } } produces a class whose instance type grows with each chained call, and the mixin helper function Timestamped<TBase extends new (...args: any[]) => {}>(B: TBase) composes behaviors without classical inheritance. Object shapes can be combined via type Merge<A, B> = Omit<A, keyof B> & B; deep merges are non-trivial and usually delegated to libraries such as type-fest.

Several hygiene rules round out the picture. Prefer interface for extensible public object shapes (so declaration merging works) and type for unions, mapped types, and conditional types; remember React's JSX parser conflicts with generics, so write <T, > with a trailing comma or <T extends unknown> inside .tsx files. Mark type-only imports with import type (and enable verbatimModuleSyntax) so build tools like Babel and esbuild can erase them cleanly, and use isolatedModules when each file needs to be transpilable in isolation. Generate .d.ts files alongside the build with "declaration": true, augment existing modules via declare module 'lib' and global scope via declare global { ... }. Favor unions of string literals over enum for tree-shakability, and reach for const enum only when cross-module isolation isn't a concern. React-specific typings round things out: React.ComponentProps<typeof MyComponent>, React.ReactNode for children, React.MouseEvent<HTMLButtonElement> for events, and the function List<T>({ items, render }: { items: T[]; render: (x: T) => JSX.Element }): JSX.Element pattern for generic components. The recurring theme is that explicit, narrow types—formed through generics, conditional types, mapped types, and satisfies—buy compile-time guarantees that scale with the complexity of your domain without paying anything at runtime.

Frequently asked questions

Basic generic function signature?

function identity<T>(x: T): T { return x; }

Exhaustiveness check?

Default branch: const _exhaustive: never = shape; — TS errors if a union member isn't handled.

Type a higher-order function preserving signature?

function wrap<F extends (...a:any[])=>any>(f:F):F — keeps parameter and return types.

Exclude&lt;T, U&gt;?

Removes U from T: Exclude<'a'|'b'|'c', 'a'> // 'b'|'c'

Conditional return type pattern?

function get<T extends 'a'|'b'>(t: T): T extends 'a' ? number : string

keyof on type with index signature?

keyof { [k: string]: number } // string | number — numeric keys are also string-indexable.

Constraint chain example?

<T extends object, K extends keyof T, V extends T[K]>

Type the props of a React component?

type Props = React.ComponentProps<typeof MyComponent>

Generic constraint requiring constructor?

C extends new (...args: any[]) => any

verbatimModuleSyntax?

Requires `import type` for type-only imports — eliminates ambiguity for build tools.

Drill this topic

241 flashcards on Typescript Generics Patterns And Constraints — free, no signup needed to start.

Study Typescript Generics Patterns And Constraints flashcards

LearnWiki pages are generated with AI assistance from LearnCoachAssist's reviewed study catalog and may contain errors — verify anything critical against your course materials.