Skip to main content

loom-typescript

TypeScript language expertise for type-safe, production-quality code.

Source facts

Repository
cosmix/loom
Last source activity
September 24, 2026 at 17:08
Detected SKILL.md language
English
Stars
61
Forks
4

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
loom-typescript
description
TypeScript language expertise for type-safe, production-quality code.
triggers
["typescript","ts","tsx","interface","generic","union","intersection","discriminated union","type guard","type assertion","utility types","conditional types","mapped types","satisfies","zod","trpc","prisma","react","node","nodejs","deno","bun","oxc","oxlint","oxfmt","rolldown","tsdown","vite","eslint","prettier","npm","pnpm","[Truncated]"]
# TypeScript Language Expertise ## Overview Type-safe, production-quality TypeScript: the type system's sharp edges, strict-mode config that actually moves the needle, the type-vs-runtime boundary, and framework patterns (zod/tRPC/Prisma/React/Express). Assumes fluency with JS and basic TS — this is reference for the traps and idioms that bite experienced engineers. Toolchain assumed throughout: **Bun** (install/run/test), **Vite** (app dev server and bundler — Rolldown bundling, Oxc transforms and minifier, Lightning CSS), **tsdown** (library bundler on Rolldown), **oxlint** (lint), **oxfmt** (format), `tsc --noEmit` (type-check). ESLint, Prettier, esbuild, and Babel are the legacy equivalents — do not add them to a project that has this stack. See **Toolchain** below. ## Type System Essentials ### Generics ```typescript // Constraints + default type param; `this` return for fluent chaining interface HasId { id: string } class Repo<T extends HasId = HasId> { private items = new Map<string, T>(); save(item: T): this { this.items.set(item.id, item); return this; } find(id: string): T | undefined { return this.items.get(id); } } // Return `as const` to preserve a tuple/literal shape instead of widening function pair<A, B>(a: A, b: B) { return [a, b] as const; } // readonly [A, B] ``` ### Utility types | Utility | Result | | ----------------------------- | --------------------------------------------- | | `Partial<T>` / `Required<T>` | all props optional / required | | `Readonly<T>` | all props `readonly` (shallow) | | `Pick<T,K>` / `Omit<T,K>` | keep / drop keys `K` | | `Record<K,V>` | object with keys `K`, values `V` | | `Extract<U,V>` / `Exclude<U,V>` | keep / drop union members of `U` assignable to `V` | | `NonNullable<T>` | strip `null` / `undefined` | | `ReturnType<F>` / `Parameters<F>` | function return type / param tuple | | `Awaited<T>` | recursively unwrap `Promise` (prefer over a hand-rolled `Unwrap`) | ```typescript // Derive types from values so they can't drift: function createUser(name: string, email: string): User { /* … */ } type NewUser = ReturnType<typeof createUser>; // User type NewUserArgs = Parameters<typeof createUser>; // [string, string] ``` ### Conditional types & `infer` ```typescript type Elem<T> = T extends (infer U)[] ? U : never; type Ret<T> = T extends (...a: any[]) => infer R ? R : never; // Conditionals DISTRIBUTE over naked union type params: type ToArray<T> = T extends any ? T[] : never; type A = ToArray<string | number>; // string[] | number[] // Wrap both sides in a 1-tuple to DISABLE distribution: type ToArray1<T> = [T] extends [any] ? T[] : never; type B = ToArray1<string | number>; // (string | number)[] ``` ### Mapped types ```typescript type Mutable<T> = { -readonly [K in keyof T]: T[K] }; type Optional<T> = { [K in keyof T]+?: T[K] }; // Key remapping via `as` (TS 4.1+): rename or filter keys type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }; type PickByValue<T, V> = { [K in keyof T as T[K] extends V ? K : never]: T[K] }; // PickByValue<{a:string;b:number}, string> → { a: string } ``` ### Discriminated unions & exhaustiveness Model each valid state as a variant with a shared literal discriminant; `switch` on it narrows each arm, and an `assertNever` default turns "added a variant, forgot a case" into a compile error. ```typescript type State = | { status: "idle" } | { status: "loading" } | { status: "success"; data: User[] } | { status: "error"; error: Error }; function assertNever(x: never): never { throw new Error(`Unreachable: ${JSON.stringify(x)}`); } function render(s: State): string { switch (s.status) { case "idle": return "Click to load"; case "loading": return "Loading…"; case "success": return `Loaded ${s.data.length}`; // s narrowed → data exists case "error": return s.error.message; // s narrowed → error exists default: return assertNever(s); // ← new unhandled variant = type error } } ``` ⚠ Exhaustiveness relies on a *finite* discriminant. A **numeric enum** discriminant accepts any `number`, so `assertNever` won't catch a missing case — use string-literal unions or `as const` objects (see Anti-Patterns). ### Type guards, assertion functions, `unknown` vs `any` - `any` disables checking **transitively** — it silently poisons every expression it flows into. `unknown` is the safe top type: assignable *from* anything, assignable *to* nothing until you narrow. - Type external inputs (`JSON.parse`, `fetch().json()`, `catch` vars, `process.env` shapes) as `unknown` and narrow with `typeof` / `instanceof` / `in` / a validator before use. ```typescript function isCat(a: Cat | Dog): a is Cat { return "meow" in a; } // Assertion function: narrows for the rest of the scope, throws otherwise function assertNonNull<T>(v: T | null | undefined, msg?: string): asserts v is T { if (v == null) throw new Error(msg ?? "value is null/undefined"); } ``` ⚠ An explicit `x is T` predicate is **trusted, not verified** by the compiler — a wrong body is as unsafe as `as`. Prefer letting TS *infer* the predicate (TS 5.5+, see Gotchas); reserve explicit `is` for what inference can't express, and unit-test those. ## tsconfig & Strict Mode `module`/`moduleResolution` must match where the output runs — there is no universal template. Pick ONE of the two below; never copy a `nodenext` config into a bundler app or vice versa. ### Node app or published library — `module: nodenext` Requires explicit `.js` extensions on relative imports plus `"type": "module"` in `package.json` (or `.mts`/`.cts`). `nodenext` implies a matching `lib`/`target`, so an explicit `"lib"` is redundant here. ```json { "compilerOptions": { "module": "nodenext", "verbatimModuleSyntax": true, "outDir": "./dist", "rootDir": "./src", "declaration": true, "declarationMap": true, "sourceMap": true, "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` ### Bundler app (Vite / Rolldown) Extensionless relative imports work; the bundler emits, so `tsc` only type-checks (`noEmit`). ⚠ Never use `moduleResolution: bundler` for a **published library** — it's "infectious": emitted `.d.ts` files carry extensionless relative imports that break Node.js ESM consumers. Vite transpiles TS with Oxc, which strips types without checking them — `tsc --noEmit` stays the type gate (`oxlint --type-aware` covers the lint half, not the compiler's). ```json { "compilerOptions": { "module": "esnext", "moduleResolution": "bundler", "verbatimModuleSyntax": true, "noEmit": true, "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true }, "include": ["src/**/*"] } ``` ### Flags beyond `strict` `strict: true` does NOT include these — opt in explicitly (TS 5.9 `tsc --init` now enables them for new projects): - **`noUncheckedIndexedAccess`** — adds `| undefined` to array subscripts and index-signature access (`arr[i]: T | undefined`). ⚠ NOT applied to named properties, NOT to `for...of` loop variables (by design, microsoft/TypeScript#42622), NOT to `Object.values()` — so it's no safety net when iterating. - **`exactOptionalPropertyTypes`** — `obj.x = undefined` becomes an error for `x?: "a" | "b"`; only *deleting* the key makes the property absent. Matters for `"x" in obj` checks and serialization round-trips. - **`useUnknownInCatchVariables`** (on via `strict` since 4.4) — `catch` vars are `unknown`; guard with `instanceof Error`. - **`verbatimModuleSyntax`** — modern module-safety baseline; supersedes the now-no-op `importsNotUsedAsValues`/`preserveValueImports` (see Modules). - **`isolatedModules`** — forbids constructs single-file transpilers can't handle (re-exporting a type without `type`, `const enum` inlining); required for single-file transpilers — Oxc (Vite, tsdown), Bun's transpiler, esbuild, swc, Babel. ### Checklist: tsconfig - [ ] Exactly one of `nodenext` / `bundler` chosen to match the runtime — not mixed - [ ] `strict: true` PLUS `noUncheckedIndexedAccess` + `exactOptionalPropertyTypes` (they aren't in `strict`) - [ ] `verbatimModuleSyntax: true`; type-only imports carry the `type` modifier - [ ] Library build: `declaration: true`, NOT `moduleResolution: bundler`, no `const enum` in public `.d.ts` - [ ] `isolatedModules: true` if any non-`tsc` transpiler is in the build ## Toolchain: Bun + Vite + Oxc One stack, no overlap: **Bun** installs, runs scripts, and runs `bun test`; **Vite** builds apps (Rolldown bundler, Oxc transform + minifier, Lightning CSS); **tsdown** builds libraries (Rolldown); **oxlint** lints; **oxfmt** formats; `tsc --noEmit` type-checks. Oxc replaces ESLint, Prettier, esbuild, and Babel — never add those to a project that has this stack, and never run two formatters over the same files; a second linter is acceptable only as a temporary migration bridge (see oxlint below). ```bash bun add -D typescript oxlint oxfmt bunx oxlint --init # writes .oxlintrc.json bunx oxfmt --init # writes .oxfmtrc.json (--migrate=prettier converts an existing Prettier config) bun add -D oxlint-tsgolint # needed by "typeAware": true in the config below; drop both to skip type-aware rules ``` `package.json` scripts: ```json { "scripts": { "typecheck": "tsc --noEmit", "lint": "oxlint --deny-warnings", "lint:fix": "oxlint --fix", "format": "oxfmt", "format:check": "oxfmt --check", "check": "bun run typecheck && bun run lint && bun run format:check" } } ``` ### oxlint - Config is `.oxlintrc.json`. Rules are grouped in categories; only `correctness` is on by default — opt into `suspicious`, `perf`, `pedantic` per project rather than `all`. - Setting `plugins` REPLACES the default set (`eslint`, `typescript`, `unicorn`, `oxc`) — list the defaults you keep alongside the ones you add (`import`, `promise`, `vitest`, `react`, `jsx-a11y`, ...). - `oxlint --fix` applies only safe fixes; `--fix-suggestions` and `--fix-dangerously` can change behavior — review the diff. - Type-aware rules (`typescript/no-floating-promises`, `typescript/no-unsafe-assignment`, ...) need `oxlint-tsgolint` plus `"options": { "typeAware": true }` (or `oxlint --type-aware`). They run on the native TypeScript compiler (tsgo), so the tsconfig must be tsgo-clean — no `baseUrl` — and `include` should stay tight or the pass gets slow. - Migrating from ESLint: `bunx -p @oxlint/migrate oxlint-migrate` converts a flat config to `.oxlintrc.json`. If a rule oxlint lacks must stay, keep ESLint for that rule only, add `eslint-plugin-oxlint` to turn off everything oxlint already covers, and run `oxlint && eslint` so the fast pass fails first. ```json { "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": ["eslint", "typescript", "unicorn", "oxc", "import", "promise"], "categories": { "correctness": "error", "suspicious": "warn" }, "rules": { "typescript/no-explicit-any": "error",
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub