skills/typescript-strict-patterns/SKILL.md
Use when writing or reviewing any TypeScript code. Covers discriminated unions, branded types, Zod at boundaries, const arrays over enums, and safe access patterns.
npx skillsauth add eins78/skills typescript-strict-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Project setup — read
project-setup.mdwhen bootstrapping a new project or changing tsconfig / ts-reset / type-fest. ESLint baseline — readeslint.config.mjswhen adding or tweaking lint rules.
Model variants as discriminated unions — never bags of optional properties:
// GOOD — each variant carries exactly its data
type Result =
| { status: "ok"; data: string }
| { status: "error"; message: string };
// Exhaustive check helper — will fail to compile if a variant is missed
function assertNever(x: never): never {
throw new Error(`Unexpected: ${JSON.stringify(x)}`);
}
function handle(r: Result) {
switch (r.status) {
case "ok": return r.data;
case "error": return r.message;
default: assertNever(r); // compile error if a case is missing
}
}
Use satisfies never or the assertNever helper at the default: branch. ESLint's switch-exhaustiveness-check enforces this at lint time.
Prevent accidental interchange of structurally identical types with a brand:
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function getUser(id: UserId) { /* ... */ }
const uid = "abc" as UserId; // cast once at the boundary
getUser(uid); // OK
getUser("abc"); // compile error — plain string is not UserId
Brand at system boundaries (API response parsing, DB reads). Internal code then carries the brand without further casts.
Enforce string formats at the type level:
type HexColor = `#${string}`;
type Route = `/${string}`;
type EventName = `on${Capitalize<string>}`;
function setColor(c: HexColor) { /* ... */ }
setColor("#ff0000"); // OK
setColor("red"); // compile error
Useful for config keys, route paths, and event names where runtime validation is overkill but typos are common.
! or as in Production CodeNon-null assertions (!) and type assertions (as) are banned in production code. They hide type errors. Allowed in test files where the tradeoff is acceptable (enforced by ESLint config).
Replacements:
obj.prop!: const { name = '' } = config;.at() + nullish coalescing instead of arr[0]!: const first = arr.at(0) ?? fallback;value as Foo: narrow with a type guard, then the type flows naturally.Never use enum (enforced by ESLint). Use as const arrays with derived types:
const STATUSES = ["pending", "active", "done"] as const;
type Status = (typeof STATUSES)[number];
At system boundaries where Zod already validates, prefer z.enum(STATUSES) — it gives you the union type and runtime validation in one step.
Use Zod schemas as the single source of truth for data crossing system boundaries (disk I/O, env vars, API responses, config files). Derive types with z.infer<> — never duplicate a hand-written interface alongside a schema.
export const sessionMetaSchema = z.object({
token: z.string(),
status: z.enum(["running", "completed", "error"]),
});
export type SessionMeta = z.infer<typeof sessionMetaSchema>;
safeParse() for data that may be corrupt (disk reads, JSONL) — skip gracefullyparse() for startup validation (env vars) where failure is fatalWith noUncheckedIndexedAccess, bracket access returns T | undefined. Always narrow:
.at(index) — clearer intent than bracket accessif (item !== undefined) or ??.find(), .filter(), or destructuring over index accessUse type-fest as a reference catalog when strict patterns make code verbose. Browse its source for solutions like SetRequired, Simplify, JsonValue. Copy the single type definition you need into src/types/ with attribution. Don't add the full package as a dependency — keep the dependency graph small.
If you encounter a pattern that doesn't work with newer TypeScript versions, a missing pattern, or incorrect guidance in this skill, don't just work around it — fix the skill:
https://github.com/eins78/agent-skills on a new branch, fixing the issue directlyhttps://github.com/eins78/agent-skills with: what failed, the actual behavior, and the suggested fixNever silently work around a skill gap. The fix benefits all future sessions.
development
Use when facing technical uncertainty, unproven architecture, or building a large feature where agents or humans risk getting lost in details before confirming the architecture works. Prevents horizontal layer-by-layer building that delays integration feedback.
tools
Use when sending commands to tmux panes, reading pane output, creating windows/panes, or monitoring tmux sessions. Covers reliable targeting, synchronization, and output capture patterns.
tools
Use when converting a PDF into a fold-and-print booklet (zine) — A4 sheets, double-sided, short-edge flip, fold to A5. Triggers: make a zine, make a booklet, booklet PDF, imposition, fold-and-print, 2-up booklet, print as booklet, signature imposition, pdf-zine, pdf2zine, bookletimposer. Wraps the `pdf2zine` Docker-based CLI; prefer it over hand-rolled Ghostscript or pdfjam scripts.
development
Use when converting documents between formats — HTML, Markdown, DOCX, PDF, LaTeX, EPUB, reStructuredText, Org, JIRA, CSV, Jupyter notebooks, slides, and 60+ others. Triggers: convert file, export to PDF, make a PDF, turn this into markdown, HTML to markdown, DOCX to markdown, markdown to DOCX, generate slides, create EPUB, format conversion, pandoc, document conversion. Always prefer pandoc over ad-hoc conversion scripts.