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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- typescript-generics Installs to ~/.agents/skills/typescript-generics/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install typescript-generics The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project typescript-generics Installs to .agents/skills/typescript-generics/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/typescript-generics.zipunzip -o typescript-generics.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) typescript-generics PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- You are typing React props, refs and polymorphic components: use typescript-react-types
- You need to understand one specific compiler error message: use typescript-type-errors
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.
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
- Read
AGENTS.md, the TypeScript version (consttype parameters need 5.0,NoInfer5.4, inferred type predicates 5.5) andtsconfigstrictness. - Write the call sites first, as you want them to look, including one that should fail.
- 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
| Signal | Do |
|---|---|
| Parameter type relates two places (input to output, arg to arg) | Generic |
| Type parameter appears once in the signature | Remove it; use the constraint type directly |
Caller must always pass <T> explicitly | Inference is broken; restructure so T comes from an argument |
| Several unrelated behaviors depending on a flag | Overloads or separate functions, not a conditional return type |
| Return type depends on a literal argument | Generic 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 problem | Fix |
|---|---|
Literal widened to string | const type parameter, or as const at the call site |
| T inferred from the wrong argument | NoInfer<T> on the arguments that should not drive it |
T becomes unknown | T 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 error | Infer 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>givesstring[] | number[]. Wrap in a tuple to stop it:[T] extends [U] ? ... : .... neverdistributes tonever. 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 objectaccepts arrays and functions; useRecord<string, unknown>for plain objects (and know that interfaces without index signatures are not assignable to it).keyof Tincludesnumber | symbolkeys; intersect withstringfor 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
.tsxneed<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 --typecheckortsc --noEmiton 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.
--extendedDiagnosticsinstantiation count did not jump on shared types.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
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.