TypeScript Generics skill for Codex

Types callers never have to spell out. Codex writes the desired call sites first, then the generic signature, and removes type parameters that do not relate two positions. It fixes widening with const type parameters and satisfies, steers inference with NoInfer, tames distributive conditional types, derives types from runtime values, and locks it all down with expectTypeOf tests while watching compiler instantiation counts.

  • Languages
  • $typescript-generics
  • 190 lines
  • 2 files
  • ~1,850 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- typescript-generics

Installs to ~/.agents/skills/typescript-generics/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $typescript-generics, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Designs TypeScript generic APIs and type-level code - constraints, inference from arguments, const type parameters, NoInfer, conditional types with infer, distributive behavior, mapped types with key remapping, template literal types and variadic tuples - plus type tests with expectTypeOf or tsd. Use when a user asks to make a function or hook generic, derive types from a config object or schema, write a utility type, fix inference that widens to string or unknown, or simplify type gymnastics that slow the compiler.

Use it when

  • A function, hook or class should work over several types without losing precision
  • Types should be derived from a config object, schema or constant array
  • Inference widens literals or picks the wrong type argument
  • Type-level code is unreadable or slows the compiler

Use something else when

How to check the result

Type tests with expectTypeOf or tsd pass, call sites compile with no explicit type arguments, and a deliberately wrong call fails at the caller's argument.

The mistake it prevents

Adding a type parameter that appears only once in the signature, which buys nothing and makes errors and hover text harder to read.

~/.agents/skills/typescript-generics/SKILL.md

--- name: typescript-generics description: Designs TypeScript generic APIs and type-level code - constraints, inference from arguments, const type parameters, NoInfer, conditional types with… license: MIT ---

Done well means: callers never write type arguments by hand because inference does the work, error messages point at the caller’s mistake, the types are tested like code, and nothing is more generic than the use cases require.

Before writing a generic

  1. Read AGENTS.md, the TypeScript version (const type parameters need 5.0, NoInfer 5.4, inferred type predicates 5.5) and tsconfig strictness.
  2. Write the call sites first, as you want them to look, including one that should fail.
  3. Only then write the signature. If no call site needs the relationship a type parameter expresses, do not add one.

When a type parameter earns its place

SignalDo
Parameter type relates two places (input to output, arg to arg)Generic
Type parameter appears once in the signatureRemove it; use the constraint type directly
Caller must always pass <T> explicitlyInference is broken; restructure so T comes from an argument
Several unrelated behaviors depending on a flagOverloads or separate functions, not a conditional return type
Return type depends on a literal argumentGeneric with a constraint, or overloads for 2-3 cases
// Bad: T used once; same as (x: unknown) => void
function log<T>(x: T): void { console.log(x); }

// Good: T links input to output
function first<T>(xs: readonly T[]): T | undefined { return xs[0]; }

Constraints and key relationships

function pick<T extends object, K extends keyof T>(obj: T, keys: readonly K[]): Pick<T, K> {
  const out = {} as Pick<T, K>;
  for (const k of keys) out[k] = obj[k];
  return out;
}

const u = pick({ id: 1, name: "a", email: "x" }, ["id", "name"]); // { id: number; name: string }

The single as on an empty accumulator is the accepted cost of building objects incrementally; keep it local and never cast the function’s inputs.

Controlling inference

// const type parameter: keep literals instead of widening to string[]
function defineRoutes<const R extends readonly string[]>(routes: R): R { return routes; }
const routes = defineRoutes(["/", "/users"]);         // readonly ["/", "/users"]

// NoInfer: make one argument the source of truth
function createStore<S>(initial: S, reset: NoInfer<S>): Store<S> { /* ... */ }
createStore({ count: 0 }, { count: "zero" });          // error on reset, not a widened S

// satisfies: check against a type without losing the literal shape
const config = {
  retries: 3,
  mode: "fast",
} satisfies Partial<Options>;                          // config.mode is "fast", not string
Inference problemFix
Literal widened to stringconst type parameter, or as const at the call site
T inferred from the wrong argumentNoInfer<T> on the arguments that should not drive it
T becomes unknownT only appears in a position TS cannot infer from (return type, callback return); add an argument that carries it
Union inferred where you wanted an errorInfer from one parameter, check the others with NoInfer

Conditional types

type Awaited2<T> = T extends PromiseLike<infer U> ? Awaited2<U> : T;
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type Params<F> = F extends (...args: infer P) => any ? P : never;   // built-in: Parameters<F>
  • Conditional types distribute over unions when the checked type is a naked type parameter: ToArray<string | number> gives string[] | number[]. Wrap in a tuple to stop it: [T] extends [U] ? ... : ....
  • never distributes to never. Detect it with [T] extends [never].
  • Prefer built-ins (ReturnType, Parameters, Awaited, Extract, Exclude, NonNullable) over hand-rolled versions.
  • Conditional return types do not narrow inside the implementation; you will need a cast or overloads. Overloads are usually clearer for public APIs.

Mapped types and key remapping

type Getters<T> = {
  [K in keyof T & string as `get${Capitalize<K>}`]: () => T[K];
};
type EventHandlers<E extends Record<string, unknown>> = {
  [K in keyof E as `on${Capitalize<K & string>}`]?: (payload: E[K]) => void;
};
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T];

Homomorphic mapped types ([K in keyof T]) preserve optional and readonly modifiers; remapped ones with as keep them too, but mapping over a union of strings does not.

Variadic tuples and typed pipelines

function withRetry<A extends unknown[], R>(fn: (...args: A) => Promise<R>, times = 3) {
  return async (...args: A): Promise<R> => {
    let last: unknown;
    for (let i = 0; i < times; i++) {
      try { return await fn(...args); } catch (e) { last = e; }
    }
    throw last;
  };
}

Capturing A extends unknown[] keeps parameter names and optionality in editor hints.

Deriving types from values

Make runtime data the single source of truth and derive types from it:

const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number];

const handlers = {
  created: (p: { id: string }) => {},
  deleted: (p: { id: string; hard: boolean }) => {},
} as const;
type EventName = keyof typeof handlers;
type Payload<E extends EventName> = Parameters<(typeof handlers)[E]>[0];

With schema libraries, derive from the schema (z.infer<typeof Schema>) rather than writing a parallel interface.

Testing types

import { expectTypeOf, test } from "vitest";

test("pick keeps only requested keys", () => {
  const r = pick({ a: 1, b: "x" }, ["a"]);
  expectTypeOf(r).toEqualTypeOf<{ a: number }>();
  // @ts-expect-error  key not on object
  pick({ a: 1 }, ["z"]);
});

Without Vitest, use tsd or a type-tests.ts file compiled by tsc --noEmit containing @ts-expect-error lines. Every utility type needs at least one positive and one negative case.

Compile-time performance

  • Deep recursive conditional types hit TS2589 (“excessively deep”). Add a depth counter tuple or restructure to tail-recursive form (TS evaluates tail-recursive conditionals up to 1000 levels).
  • Large unions in template literal types multiply (TS2590). Constrain inputs before combining.
  • Name intermediate types with interfaces or aliases; interfaces are cached, anonymous intersections are recomputed.
  • Measure with tsc --noEmit --extendedDiagnostics (check “Instantiations”) before and after a change to shared types.

Gotchas

  • T extends object accepts arrays and functions; use Record<string, unknown> for plain objects (and know that interfaces without index signatures are not assignable to it).
  • keyof T includes number | symbol keys; intersect with string for template literals.
  • Default type parameters (<T = string>) are used only when inference has nothing to work with; they do not override inference.
  • Generic React components with arrow functions in .tsx need <T,> to avoid JSX parsing.
  • A clever type nobody can read is a maintenance cost. If a type needs more than ~10 lines, add a comment with an example input and output.

Verification

  • Type tests pass (vitest --typecheck or tsc --noEmit on the type test files).
  • Call sites compile without explicit type arguments; the failing example fails with an error on the caller’s argument, not deep inside the library.
  • --extendedDiagnostics instantiation count did not jump on shared types.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "TypeScript Generics"
  short_description: "Types callers never have to spell out"
  default_prompt: "$typescript-generics Make this fetch helper generic so the response type comes from the route"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc