cheat sheet

Generics

TypeScript generics allow writing reusable, type-safe code that works over many types. Covers generic functions, interfaces, classes, constraints, keyof, conditional types, and the infer keyword.

Generics

What it is

Generics allow writing type-safe code that works over many types without sacrificing type information. Instead of using any (which loses type safety) or writing separate overloaded functions for each type, generics accept a type parameter — a placeholder filled in when the function or type is used. Think of them as variables for types.

Basic generic function

A generic function accepts a type parameter <T> and uses it to link the types of its inputs and outputs. This preserves type information that any would discard, letting the caller get back the exact type they passed in.

typescript
// Without generics — type information lost
function identity(arg: any): any {
  return arg;
}

// With generics — type information preserved
function identity<T>(arg: T): T {
  return arg;
}

const s = identity("hello"); // type: string
const n = identity(42);      // type: number
const b = identity(true);    // type: boolean

Type parameters can be inferred from arguments or supplied explicitly:

typescript
identity<string>("hello"); // explicit
identity("hello");          // inferred — preferred

Generic interfaces and type aliases

Generic interfaces and type aliases attach type parameters to a named shape, letting you define a single structure that works across many types. Prefer interfaces when you want a name that shows up in error messages and can be extended; prefer type aliases for unions, tuples, and complex compositions.

typescript
interface Box<T> {
  value: T;
  transform<U>(fn: (val: T) => U): Box<U>;
}

type Pair<A, B> = {
  first: A;
  second: B;
};

type Maybe<T> = T | null | undefined;

// Usage
const box: Box<number> = {
  value: 42,
  transform: (fn) => ({ value: fn(42), transform: () => { throw 0; } }),
};

const coords: Pair<number, number> = { first: 10, second: 20 };
const name: Maybe<string> = null;

Multiple type parameters

A generic can declare more than one type parameter when its inputs have independent types that should all be tracked. Keep the count low — three or more type parameters usually signals a design worth reconsidering.

typescript
function pair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}

const entry = pair("id", 123); // type: [string, number]

function merge<A, B>(a: A, b: B): A & B {
  return { ...a as object, ...b as object } as A & B;
}

const merged = merge({ name: "Alice" }, { age: 30 });
// type: { name: string } & { age: number }

Default type parameters

A default type parameter (<T = SomeType>) supplies a fallback when the caller does not provide one explicitly. Defaults reduce friction for the common case while letting power users override the type — the same idea as default function parameters, lifted to the type level.

typescript
type Container<T = string> = {
  items: T[];
  count: number;
};

const strings: Container = { items: ["a", "b"], count: 2 };     // T defaults to string
const numbers: Container<number> = { items: [1, 2], count: 2 }; // explicit T

Default type parameters are especially useful in complex generic types where most callers use the common case. Once a parameter has a default, every parameter after it must also have a default — exactly mirroring the function-argument rule.

typescript
type Result<Value = unknown, Err = Error> = { ok: true; value: Value } | { ok: false; err: Err };

type R1 = Result<string>;        // value: string, err: Error
type R2 = Result<number, never>; // value: number, never thrown
type R3 = Result;                // value: unknown, err: Error

Defaults can also reference earlier type parameters — the same scope rules as function parameters:

typescript
type Pair<A, B = A> = { first: A; second: B };

type P1 = Pair<string>;          // { first: string; second: string }
type P2 = Pair<string, number>;  // { first: string; second: number }

Verify defaults round-trip through tsc:

bash
npx tsc --noEmit

Output:

lua
(no outputexit code 0)

Bounded generics — <T extends U>

A bounded generic (often called a "constrained generic") declares an upper bound on the type parameter using extends. Inside the generic body, TypeScript treats T as having at least the structure of U, so you can access any property U guarantees without losing the caller's narrower type. Constraints are the single most useful tool for keeping generic code both reusable and type-safe.

Use extends to restrict what types a type parameter can be:

typescript
// T must be an object
function keys<T extends object>(obj: T): Array<keyof T> {
  return Object.keys(obj) as Array<keyof T>;
}

// T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("hello", "world");    // works — strings have .length
longest([1, 2], [3, 4, 5]);  // works — arrays have .length
longest(10, 20);               // Error — numbers don't have .length

Multiple constraints are combined with intersection:

typescript
function processItem<T extends object & { id: string }>(item: T): string {
  return item.id;
}

Constraint vs. parameter widening

A subtle but important rule: the caller's type is preserved through a constrained generic — the constraint is only a minimum, not a target.

typescript
function tag<T extends { id: string }>(x: T): T & { tagged: true } {
  return { ...x, tagged: true as const };
}

const result = tag({ id: "u_1", name: "Alice", age: 30 });
// result: { id: string; name: string; age: number; tagged: true }
// NOT widened to { id: string; tagged: true }

If you wrote (x: { id: string }) instead, the return type would lose name and age. The generic remembers the caller's exact shape.

Constraining one parameter by another

A type parameter can be constrained by an earlier one, so the relationship between two inputs is enforced at the type level. This is how K extends keyof T works.

typescript
function pluckOne<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Alice" };
pluckOne(user, "name"); // string
// pluckOne(user, "missing"); // Error — not a key of typeof user

Constraining with infer for refined inference

When the body of a generic needs to extract a sub-type from the input, combine a constraint with a conditional infer to surface that sub-type as another parameter — the foundation of ReturnType and friends.

typescript
// Constrain F to a function, infer its return type R for use in the signature
function memoize<F extends (...args: any[]) => any, R extends ReturnType<F>>(
  fn: F
): (...args: Parameters<F>) => R {
  const cache = new Map<string, R>();
  return (...args) => {
    const key = JSON.stringify(args);
    if (!cache.has(key)) cache.set(key, fn(...args) as R);
    return cache.get(key)!;
  };
}

const slow = (n: number) => n * 2;
const fast = memoize(slow);
console.log(fast(21));

Output:

code
42

The extends ReturnType<F> constraint refines R inside the body without forcing the caller to specify it.

keyof with generics

keyof T produces a union of all known keys of T. Combined with generics, it enables type-safe property access:

typescript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Alice", active: true };

const name = getProperty(user, "name");   // type: string
const id   = getProperty(user, "id");     // type: number
getProperty(user, "missing");              // Error — not a key of typeof user

Generic classes

A generic class parameterizes its instance shape so all methods share a consistent element type throughout the class body. Use them when you need a stateful container — like a stack, queue, or repository — that must work type-safely for any payload.

typescript
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }
}

const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
const top = numStack.peek(); // type: number | undefined

Conditional types with generics

A conditional type selects one of two types depending on whether T extends U. Combined with generics, this enables types that adapt their output based on what is passed in — the basis for built-in utility types like NonNullable and Awaited.

typescript
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;  // "yes"
type B = IsString<number>;  // "no"
type C = IsString<"hello">; // "yes" — string literal extends string

// Practical: extract the resolved type of a Promise
type Awaited<T> = T extends Promise<infer R> ? R : T;

type Resolved = Awaited<Promise<string>>; // string
type Direct   = Awaited<number>;          // number

The infer keyword

infer declares a type variable inside a conditional type, letting TypeScript extract and name a part of the matched type:

typescript
// Extract the return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = (x: number) => string;
type R = ReturnType<Fn>; // string

// Extract the element type of an array
type ElementType<T> = T extends (infer E)[] ? E : never;

type E = ElementType<string[]>;     // string
type F = ElementType<number[][]>;   // number[]

// Extract first parameter type
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

type G = FirstParam<(x: string, y: number) => void>; // string

Implementing ReturnType and Parameters from scratch:

typescript
type MyReturnType<T extends (...args: any[]) => any> =
  T extends (...args: any[]) => infer R ? R : never;

type MyParameters<T extends (...args: any[]) => any> =
  T extends (...args: infer P) => any ? P : never;

function add(a: number, b: number): number { return a + b; }

type AddReturn = MyReturnType<typeof add>;     // number
type AddParams  = MyParameters<typeof add>;    // [a: number, b: number]

Variance: covariance and contravariance

Variance describes how a generic type's assignability behaves when its parameter is substituted for a sub-type. A parameter is covariant when "going narrower" preserves assignability (think producers: () => Cat is assignable to () => Animal), contravariant when "going wider" preserves it (consumers: (a: Animal) => void is assignable to (c: Cat) => void), invariant when neither direction is safe, and bivariant when both directions are accepted (an unsound but ergonomic relaxation TypeScript uses for method parameters under strictFunctionTypes: false).

typescript
// Covariant position: function return type
// If Cat extends Animal, then Producer<Cat> extends Producer<Animal>
type Producer<T> = () => T;

// Contravariant position: function parameter
// If Cat extends Animal, then Consumer<Animal> extends Consumer<Cat>
type Consumer<T> = (val: T) => void;

A worked example:

typescript
interface Animal { name: string }
interface Cat extends Animal { meow(): void }

declare const animalProducer: Producer<Animal>;
declare const catProducer: Producer<Cat>;
const p: Producer<Animal> = catProducer; // OK — covariant in T
// const q: Producer<Cat> = animalProducer; // Error — not contravariant

declare const animalEater: Consumer<Animal>;
declare const catEater: Consumer<Cat>;
const e: Consumer<Cat> = animalEater; // OK — contravariant in T
// const f: Consumer<Animal> = catEater; // Error — not covariant

Variance annotations (TS 4.7+)

The in and out modifiers on a type parameter document and enforce its variance. They are particularly valuable on library types, where they let the compiler catch a future change that breaks assignability assumptions.

typescript
type Box<out T> = { get(): T };            // covariant
type Sink<in T> = { set(val: T): void };   // contravariant
type Cell<in out T> = { get(): T; set(val: T): void }; // invariant

If you mark a parameter out T and later add a method that uses T in a contravariant position, the compiler errors at the declaration site — not at some distant assignment.

typescript
// Caught at the declaration:
// type Bad<out T> = { set(v: T): void }; // Error — T should be 'in' for this usage

When to reach for in/out

SituationRecommendation
Read-only collection typesout T
Write-only sink typesin T
Mutable containersin out T (or omit and accept invariance)
Library type that callers extendAnnotate explicitly — documents intent
Application-internal helperAnnotations rarely worth the noise

NoInfer — opting out of inference (TS 5.4+)

NoInfer<T> is a utility type added in TypeScript 5.4 that excludes a position from inference. When a generic function uses the same type parameter in two argument positions, the compiler normally tries to unify the two inferred types — sometimes guessing wrongly. Wrapping one position in NoInfer<T> says "use this only for checking, infer T from somewhere else."

typescript
// Without NoInfer — `value` widens T
function createState<T>(initial: T, validator: (v: T) => boolean) {
  return { initial, validator };
}

createState("idle", (v) => v === "idle" || v === "busy");
// T inferred as string — broader than we want

With NoInfer, you can force T to be inferred from one position only:

typescript
function createState<T extends string>(initial: T, validator: (v: NoInfer<T>) => boolean) {
  return { initial, validator };
}

// Now T = "idle" exactly, and the validator's parameter must be "idle"
const s = createState("idle", (v) => v === "idle"); // OK
// createState("idle", (v) => v === "busy"); // Error — "busy" not assignable to "idle"

NoInfer is especially useful when one generic parameter is meant to be the source of truth (the literal you want to remember) and other parameters merely consume that type.

typescript
function pick<T>(options: readonly T[], defaultValue: NoInfer<T>): T {
  return options.includes(defaultValue) ? defaultValue : options[0];
}

const colors = ["red", "green", "blue"] as const;
const c = pick(colors, "red");   // OK
// pick(colors, "purple");        // Error — "purple" not assignable to typeof colors[number]

Higher-kinded simulation

TypeScript has no first-class higher-kinded types (HKTs) — you cannot abstract over a type constructor the way Haskell does with Functor f. The community has built several encoding patterns that approximate HKTs by using interface lookup, often called the URI trick (or "defunctionalization"). The pattern lets you write generic code over Array, Promise, Option, etc., picking the right concrete type at the call site.

typescript
// Declare a registry interface; concrete types extend it via declaration merging
interface URItoKind<A> {
  Array: A[];
  Promise: Promise<A>;
  Option: A | null;
}

type URI = keyof URItoKind<unknown>;

// "Kind" applies a URI to an inner type
type Kind<F extends URI, A> = URItoKind<A>[F];

interface Functor<F extends URI> {
  map<A, B>(fa: Kind<F, A>, f: (a: A) => B): Kind<F, B>;
}

// Implementations
const arrayFunctor: Functor<"Array"> = {
  map: (fa, f) => fa.map(f),
};

const optionFunctor: Functor<"Option"> = {
  map: (fa, f) => (fa === null ? null : f(fa)),
};

console.log(arrayFunctor.map([1, 2, 3], (n) => n * 2));
console.log(optionFunctor.map<number, string>(5, (n) => `n=${n}`));
console.log(optionFunctor.map<number, string>(null, (n) => `n=${n}`));

Output:

ini
[ 2, 4, 6 ]
n=5
null

The trade-offs: HKT simulation works, but error messages get noisy and inference often needs annotation help. Libraries like fp-ts and Effect use this pattern extensively; in everyday application code, conditional types and constrained generics usually suffice.

Generic functions vs generic types

A generic function introduces type parameters at the call site, inferred from each invocation. A generic type (alias, interface, class) introduces them at declaration and binds them when the type is used. They look similar but solve different problems.

typescript
// Generic function — T fresh on each call
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const a = first([1, 2, 3]);      // T = number
const b = first(["x", "y"]);     // T = string  (different T)

// Generic type — T fixed once when the type is instantiated
type Box<T> = { value: T };

const c: Box<number> = { value: 42 };
const d: Box<string> = { value: "hi" };

Use a generic function when the operation should be polymorphic per call. Use a generic type when you want a named shape that callers parameterise. Combining them — a generic class or interface whose methods are themselves generic — gives you both, and is the standard way to type containers like Map<K, V> whose .get<U>() method (in some libraries) is independently generic.

Generic constraints with infer

infer declares a fresh type variable inside the extends clause of a conditional type — letting a constraint both narrow the input and extract a sub-type. The common idiom is T extends Shape<infer X> ? X : Fallback, which only matches inputs that look like Shape<...> and binds X to the slot.

typescript
type ArrayElement<T extends readonly unknown[]> =
  T extends readonly (infer E)[] ? E : never;

type FirstParamOf<F extends (...args: any) => any> =
  F extends (first: infer P, ...rest: any) => any ? P : never;

type N = ArrayElement<readonly number[]>;          // number
type S = FirstParamOf<(x: string, y: number) => void>; // string

Combining a generic constraint with infer is how Awaited, ConstructorParameters, and ThisParameterType work — see the dedicated infer-keyword article for the full pattern catalogue.

Common patterns

Identity

typescript
const identity = <T>(x: T): T => x;

Wrapper (adds metadata)

typescript
function withTimestamp<T>(data: T): T & { timestamp: number } {
  return { ...data as object, timestamp: Date.now() } as T & { timestamp: number };
}

Mapper

typescript
function mapRecord<K extends string, V, W>(
  record: Record<K, V>,
  fn: (val: V, key: K) => W
): Record<K, W> {
  const result = {} as Record<K, W>;
  for (const key in record) {
    result[key] = fn(record[key], key);
  }
  return result;
}

const prices = { apple: 1.5, banana: 0.8 };
const doubled = mapRecord(prices, (v) => v * 2);
// { apple: 3, banana: 1.6 }

Factory

typescript
type Constructor<T> = new (...args: any[]) => T;

function createInstance<T>(cls: Constructor<T>, ...args: any[]): T {
  return new cls(...args);
}

class Logger { constructor(public prefix: string) {} }
const log = createInstance(Logger, "[INFO]");

Type-safe event emitter

typescript
type EventMap = {
  connect: { host: string; port: number };
  disconnect: { reason: string };
  message: { payload: string };
};

function createEmitter<M extends Record<string, unknown>>() {
  const listeners = new Map<keyof M, Set<(data: any) => void>>();

  return {
    on<K extends keyof M>(event: K, fn: (data: M[K]) => void): void {
      if (!listeners.has(event)) listeners.set(event, new Set());
      listeners.get(event)!.add(fn);
    },
    emit<K extends keyof M>(event: K, data: M[K]): void {
      listeners.get(event)?.forEach((fn) => fn(data));
    },
  };
}

const emitter = createEmitter<EventMap>();
emitter.on("connect", ({ host, port }) => console.log(`${host}:${port}`));
emitter.emit("connect", { host: "localhost", port: 3000 });

Performance considerations

Generics are free at runtime — TypeScript erases all type parameters before emitting JavaScript. But they are not free at compile time. Every distinct instantiation of a generic type counts as a separate type the compiler must track, and excessive generic depth can dominate tsc --noEmit time on large codebases.

ConcernSymptomMitigation
Deep recursion"Type instantiation is excessively deep" errorCap recursion with a depth counter; use tail-call style
Union explosionSlow tooltips, slow tsc, eventually OOMConstrain inputs; collapse to string/unknown where precision is unused
Conditional cascadesRepeated T extends ... ? chains in hot pathsExtract intermediate type aliases; let the compiler cache
Distributive surprisesSingle inferred shape becomes a giant unionWrap with [T] to prevent distribution
Excessive infer slotsSlow editor highlightsCombine multiple infers only when truly needed

A practical example: a DotKeys<T> that walks an unbounded depth blows up on a 6-level config object. Bound the recursion explicitly:

typescript
type DotKeys<T, D extends number = 4, P extends string = ""> =
  [D] extends [0] ? never :
  {
    [K in keyof T & string]: T[K] extends Record<string, unknown>
      ? DotKeys<T[K], Prev<D>, `${P}${K}.`>
      : `${P}${K}`;
  }[keyof T & string];

// Helper — decrement a numeric literal up to 5
type Prev<N extends number> = [-1, 0, 1, 2, 3, 4, 5][N];

Check compile cost with the diagnostic flag:

bash
npx tsc --noEmit --extendedDiagnostics

Output:

yaml
Files:                          120
Lines of Library:             40234
Lines of Definitions:         15214
Lines of TypeScript:           8421
...
Instantiations:              123456
Memory used:                  178MB
Total time:                    3.21s

If Instantiations is in the millions, hunt down the worst offenders with --generateTrace:

bash
npx tsc --noEmit --generateTrace ./trace

Output:

bash
(no console output — writes ./trace/trace.json and ./trace/types.json)

Open the trace in Chrome's chrome://tracing UI to find which file or type instantiation dominates.

Common pitfalls

  1. Passing any to a genericidentity<any>("hi") silently widens every downstream type to any. Prefer unknown when the type is genuinely unknown; reserve any for last-resort escape hatches.
  2. Over-parameterising — A function with five generic parameters is almost always a sign the design needs simpler interfaces. Three is a soft cap; beyond that, refactor.
  3. Constraint shadowingfunction f<T extends string>(x: T): string returns string instead of T, throwing away the narrow literal. Make the return type T if you want to preserve the input.
  4. Forgetting extends on keyoffunction get<T, K>(o: T, k: K): T[K] errors because K isn't known to be a key of T. Constrain with K extends keyof T.
  5. Default parameters and explicit <T> — Specifying <T> explicitly disables a default. If you want the default, omit the explicit angle brackets entirely.
  6. Mixing unknown and any constraints<T extends any> is a near-no-op and exists mostly for legacy code; <T extends unknown> is also a no-op but signals the constraint deliberately. Prefer no constraint when you mean "any type."
  7. Generic parameter leaking into a callback(cb: (x: T) => void) => void where T is a generic in a different scope confuses the compiler. Make the callback generic itself: <U>(cb: (x: U) => void) => void.
  8. Variance violation under strictFunctionTypes: false — Method parameters are bivariant in legacy mode; turning strict mode on can surface real bugs. Always enable strictFunctionTypes in new projects.
  9. Record<string, T> wideningkeyof Record<string, T> is just string. If you want enumerable keys, use a literal union Record<"a" | "b", T>.
  10. Empty interface as a constraint<T extends {}> permits any non-null value, including primitives — almost never what you want. Use <T extends object> for "actual object" or <T extends Record<string, unknown>> for "object with string keys."

Real-world recipes

Recipe 1 — Type-safe Object.entries

The built-in Object.entries returns [string, unknown][], losing the precise tuple of key-value pairs. A generic wrapper preserves them.

typescript
function entriesOf<T extends Record<string, unknown>>(
  obj: T
): Array<[keyof T & string, T[keyof T]]> {
  return Object.entries(obj) as Array<[keyof T & string, T[keyof T]]>;
}

const user = { id: 1, name: "Alice", active: true };
for (const [k, v] of entriesOf(user)) {
  console.log(`${k}=${v}`);
}

Output:

ini
id=1
name=Alice
active=true

Recipe 2 — Generic Result type with discriminated outcome

A Result<T, E> models success-or-failure with strict typing on both branches. Pair with a match helper for exhaustive case handling.

typescript
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; err: E };

function ok<T>(value: T): Result<T, never> { return { ok: true, value }; }
function err<E>(error: E): Result<never, E> { return { ok: false, err: error }; }

function match<T, E, R>(r: Result<T, E>, onOk: (v: T) => R, onErr: (e: E) => R): R {
  return r.ok ? onOk(r.value) : onErr(r.err);
}

function parsePort(raw: string): Result<number, string> {
  const n = Number(raw);
  return Number.isInteger(n) && n > 0 ? ok(n) : err(`invalid port ${raw}`);
}

console.log(match(parsePort("3000"), (p) => `port ${p}`, (e) => `oops: ${e}`));
console.log(match(parsePort("0x"), (p) => `port ${p}`, (e) => `oops: ${e}`));

Output:

yaml
port 3000
oops: invalid port 0x

Recipe 3 — Typed builder with progressively richer state

A builder pattern can use generics to track which methods have been called, refusing .build() until required fields are set.

typescript
type RequiredFields = "host" | "port";

class ClientBuilder<Filled extends keyof Config = never> {
  private config: Partial<Config> = {};

  set<K extends keyof Config>(
    key: K,
    value: Config[K]
  ): ClientBuilder<Filled | K> {
    this.config[key] = value;
    return this as unknown as ClientBuilder<Filled | K>;
  }

  build(this: ClientBuilder<RequiredFields>): Config {
    return this.config as Config;
  }
}

interface Config { host: string; port: number; tls?: boolean }

const b = new ClientBuilder()
  .set("host", "localhost")
  .set("port", 3000);

console.log(b.build());
// new ClientBuilder().set("host", "x").build(); // Error — port still missing

Output:

yaml
{ host: 'localhost', port: 3000 }

Recipe 4 — Generic retry helper with NoInfer on the fallback

Use NoInfer so the fallback value is checked against the return type without widening it.

typescript
async function retry<T>(
  fn: () => Promise<T>,
  attempts = 3,
  fallback?: NoInfer<T>
): Promise<T> {
  let last: unknown;
  for (let i = 0; i < attempts; i++) {
    try { return await fn(); } catch (e) { last = e; }
  }
  if (fallback !== undefined) return fallback;
  throw last;
}

const value = await retry(async () => 42 as const, 2, 0);
console.log(value);
// const wrong = await retry(async () => 42 as const, 2, "fallback"); // Error

Output:

code
42

Recipe 5 — Constrained mapping over branded keys

Combine a constrained generic with template literals so the mapper preserves the brand prefix on every transformed key.

typescript
type Prefixed<P extends string, K extends string> = `${P}${K}`;

function prefixKeys<P extends string, T extends Record<string, unknown>>(
  prefix: P,
  obj: T
): { [K in keyof T & string as Prefixed<P, K>]: T[K] } {
  const out = {} as Record<string, unknown>;
  for (const k in obj) out[`${prefix}${k}`] = obj[k];
  return out as { [K in keyof T & string as Prefixed<P, K>]: T[K] };
}

const r = prefixKeys("api_", { host: "localhost", port: 3000 });
// r: { api_host: string; api_port: number }
console.log(r);

Output:

yaml
{ api_host: 'localhost', api_port: 3000 }

Recipe 6 — Tagged value with generic phantom type

A phantom-type pattern (related to branded types) uses an unused generic parameter purely as a compile-time marker.

typescript
type Tagged<T, Tag extends string> = T & { readonly __tag: Tag };

type Validated<T> = Tagged<T, "Validated">;
type Sanitized<T> = Tagged<T, "Sanitized">;

function validate<T extends { id: number }>(x: T): Validated<T> {
  if (x.id <= 0) throw new Error("invalid id");
  return x as Validated<T>;
}

function persist<T>(x: Validated<T>): void {
  console.log("persist", x);
}

const v = validate({ id: 1, name: "Alice" });
persist(v);
// persist({ id: 1, name: "Alice" }); // Error — not Validated

Output:

bash
persist { id: 1, name: 'Alice' }

Recipe 7 — Generic state machine transitions

Use a generic indexed-access type to express "from state S, only certain events are allowed."

typescript
type Transitions = {
  idle:    { start: "running" };
  running: { pause: "paused"; finish: "done" };
  paused:  { resume: "running"; cancel: "done" };
  done:    {};
};

type State = keyof Transitions;
type EventOf<S extends State> = keyof Transitions[S];
type NextOf<S extends State, E extends EventOf<S>> = Transitions[S][E];

function transition<S extends State, E extends EventOf<S>>(state: S, event: E): NextOf<S, E> {
  return ({
    idle:    { start: "running" },
    running: { pause: "paused", finish: "done" },
    paused:  { resume: "running", cancel: "done" },
    done:    {},
  } as Transitions)[state][event] as NextOf<S, E>;
}

const next = transition("idle", "start");   // "running"
console.log(next);
// transition("idle", "pause"); // Error — pause not allowed from idle

Output:

arduino
running

Recipe 8 — Forwarding generics through a wrapper

A common need: wrap an existing generic function (logging, telemetry) without losing the original's type parameters. Use Parameters and ReturnType plus a passthrough generic.

typescript
function logged<F extends (...args: any[]) => any>(label: string, fn: F): F {
  return function (...args: Parameters<F>): ReturnType<F> {
    console.log(`[${label}] in:`, args);
    const out = fn(...args);
    console.log(`[${label}] out:`, out);
    return out;
  } as F;
}

const add = (a: number, b: number) => a + b;
const loggedAdd = logged("add", add);
console.log(loggedAdd(2, 3));

Output:

csharp
[add] in: [ 2, 3 ]
[add] out: 5
5