typescript-ops
TypeScript type system, generics, utility types, strict mode, and ecosystem patterns. Use for: typescript, ts, type, generic, utility type, Partial, Pick, Omit, Record, Exclude, Extract, ReturnType, Parameters, keyof, typeof, infer, mapped type, conditional type, template literal
Install
npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/typescript-ops
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
git clone https://github.com/0xDarkMatter/claude-mods.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
TypeScript Operations
Comprehensive TypeScript skill covering the type system, generics, and production patterns.
Ecosystem facts verified as of 2026-08-08 (TypeScript 7, Zod 4, Valibot 1).
Staleness check: python scripts/check-typescript-facts.py --offline asserts the
catalogued version-bearing facts (TypeScript major, zod, valibot) are still named in the
prose and the dated currency note above is present; run --live to confirm each package's
npm major still matches the documented major. Catalog: assets/typescript-facts.json.
Type Narrowing Decision Tree
How to narrow a type?
│
├─ Primitive type check
│ └─ typeof: typeof x === "string"
│
├─ Instance check
│ └─ instanceof: x instanceof Date
│
├─ Property existence
│ └─ in: "email" in user
│
├─ Discriminated union
│ └─ switch on literal field: switch (event.type)
│
├─ Null/undefined check
│ └─ Truthiness: if (x) or if (x != null)
│
├─ Custom logic
│ └─ Type predicate: function isUser(x: unknown): x is User
│
└─ Assertion (you know better than TS)
└─ as: value as string (escape hatch, avoid when possible)
Type Guard Example
interface Dog { bark(): void; breed: string }
interface Cat { meow(): void; color: string }
function isDog(pet: Dog | Cat): pet is Dog {
return "bark" in pet;
}
function handlePet(pet: Dog | Cat) {
if (isDog(pet)) {
pet.bark(); // TS knows it's Dog here
} else {
pet.meow(); // TS knows it's Cat here
}
}
Discriminated Unions
type Result<T> =
| { status: "success"; data: T }
| { status: "error"; error: string }
| { status: "loading" };
function handle<T>(result: Result<T>) {
switch (result.status) {
case "success": return result.data; // data is available
case "error": throw new Error(result.error); // error is available
case "loading": return null;
}
// Exhaustiveness check: result is `never` here
const _exhaustive: never = result;
}
Utility Types Cheat Sheet
| Utility | What It Does | Example |
|---|---|---|
Partial<T> |
All props optional | Partial<User> for update payloads |
Required<T> |
All props required | Required<Config> for validated config |
Readonly<T> |
All props readonly | Readonly<State> for immutable state |
Pick<T, K> |
Select specific props | Pick<User, "id" \| "name"> |
Omit<T, K> |
Remove specific props | Omit<User, "password"> |
Record<K, V> |
Object with typed keys/values | Record<string, number> |
Exclude<U, E> |
Remove types from union | Exclude<Status, "deleted"> |
Extract<U, E> |
Keep types from union | Extract<Event, { type: "click" }> |
NonNullable<T> |
Remove null/undefined | NonNullable<string \| null> |
ReturnType<F> |
Function return type | ReturnType<typeof fetchUser> |
Parameters<F> |
Function params as tuple | Parameters<typeof createUser> |
Awaited<T> |
Unwrap Promise type | Awaited<Promise<User>> = User |
Generic Patterns
Constrained Generics
// Basic constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Multiple constraints
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
// Default generic type
type ApiResponse<T = unknown> = {
data: T;
status: number;
};
Conditional Types
// Basic conditional
type IsString<T> = T extends string ? true : false;
// infer keyword - extract inner type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type UnwrapArray<T> = T extends (infer U)[] ? U : T;
// Distributive conditional (distributes over union)
type ToArray<T> = T extends any ? T[] : never;
// ToArray<string | number> = string[] | number[]
// Prevent distribution with wrapping
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
// ToArrayNonDist<string | number> = (string | number)[]
Mapped Types
// Make all properties optional and nullable
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Add prefix to keys
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
// Prefixed<{ name: string }, "get"> = { getName: string }
// Filter keys by value type
type StringKeys<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
Deep dive: Load ./references/generics-patterns.md for advanced type-level programming, recursive types, template literal types.
Modern Language Features (TypeScript 5.x → 6.0)
| Feature | Since | What It Gives You |
|---|---|---|
satisfies operator |
4.9 | Check a value against a type without widening it |
| Standard (TC39) decorators | 5.0 | @decorator on classes/methods without experimentalDecorators |
const type parameters |
5.0 | function f<const T>(x: T) infers literal types without as const at call sites |
using declarations |
5.2 | Explicit resource management (Symbol.dispose), auto-cleanup at scope exit |
| Inferred type predicates | 5.5 | arr.filter(x => x !== null) narrows without a hand-written x is T guard |
verbatimModuleSyntax |
5.0 | Enforces import type for type-only imports — replaces importsNotUsedAsValues |
// const type parameters (5.0) - literal inference without as const
function routes<const T extends readonly string[]>(paths: T): T { return paths; }
const r = routes(["/home", "/about"]); // readonly ["/home", "/about"], not string[]
// using declarations (5.2) - deterministic cleanup
function readConfig() {
using file = openFile("config.json"); // file[Symbol.dispose]() runs at scope exit
return parse(file.contents);
}
// Inferred type predicates (5.5) - no manual guard needed
const names = ["a", null, "b"].filter(x => x !== null); // string[], not (string | null)[]
TypeScript 6.0 (The Bridge Release)
TS 6.0 is the last release on the JavaScript-based compiler — it exists to bridge to the native (Go) compiler in TS 7, so its headline is stricter, modernised defaults:
strict: trueis the default — a tsconfig that never set it now gets full strict checks- Defaults modernised:
module: esnext,target: es2025;es2025lib ships types for Temporal,Map.getOrInsert,RegExp.escape - Legacy options removed:
moduleResolution: classic;module: amd/umd/system/none; minimumtargetis now ES2015 (es5deprecated) - Interop always on:
esModuleInterop/allowSyntheticDefaultImportscan no longer be disabled - New
--stableTypeOrderingflag eases 6.0 → 7.0 migration diffing
TypeScript 7 (Current Major — Native Compiler)
TS 7 is the Go-native rewrite (formerly tsgo), stable on npm since 2026-07-08.
Same checking semantics, ~8–16× faster typechecks — but the package ships only a
bin/tsc shim, no JavaScript compiler API: require('typescript') throws
MODULE_NOT_FOUND on 7.0.x (the API returns in 7.1+). Consequences that gate
adoption:
- Repo tooling using the TS programmatic API (
ts.createSourceFile, custom lint scripts, codemods, typescript-eslint) must go AST-free, switch parser, or pin an alias - Tools typechecking embedded languages (vue-tsc, svelte-check, Astro, MDX) are pinned to TS 6 until they port to the 7.1+ API — a real stack-selection input
- A tsconfig already on TS 6 defaults adopts directly (the 6.0 bridge is skippable);
baseUrlis a hard error (TS5102) - Running a
typescript5fallback alias alongside 7 makes barenpx tscambiguous — scripts must use explicit compiler paths during the soak
Deep dive: Load ./references/ts7-native-compiler.md for the no-JS-API workarounds,
dual-install bin ambiguity, ecosystem lockout table, measured adoption benchmarks (12.1×),
and the go/no-go checklist.
tsconfig Quick Reference
{
"compilerOptions": {
// Strict mode (default in TS 6; state it explicitly anyway)
"strict": true, // Enables all strict checks
"noUncheckedIndexedAccess": true, // arr[0] is T | undefined
// Module system (TS 6 defaults to module: esnext; interop is always on)
"module": "esnext", // or "nodenext" for Node
"moduleResolution": "bundler", // or "nodenext"
// Output (TS 6 defaults target to es2025; min supported is es2015)
"target": "es2022",
"outDir": "dist",
"declaration": true, // Generate .d.ts
"sourceMap": true,
// Paths — tsconfig-relative; don't add baseUrl (TS 7 hard-errors on it, TS5102)
"paths": { "@/*": ["./src/*"] },
// Strictness extras
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
Deep dive: Load ./references/config-strict.md for strict mode migration, monorepo config, project references.
Common Gotchas
| Gotcha | Why | Fix |
|---|---|---|
any leaks |
any disables type checking for everything it touches |
Use unknown + narrowing instead |
as assertions hide bugs |
Assertion doesn't check at runtime | Use type guards or validation (Zod) |
enum quirks |
Numeric enums are not type-safe, reverse mappings confuse | Use as const objects or string literal unions |
object vs Record vs {} |
{} matches any non-null value, object is non-primitive |
Use Record<string, unknown> for "any object" |
| Array index access | arr[999] returns T not T \| undefined by default |
Enable noUncheckedIndexedAccess |
| Optional vs undefined | { x?: string } allows missing key, { x: string \| undefined } requires key |
Be explicit about which you mean |
! non-null assertion |
Silences null checks, no runtime effect | Use ?? defaultValue or proper null check |
| Structural typing surprise | { a: 1, b: 2 } assignable to { a: number } |
Use branded types for nominal typing |
Branded / Nominal Types
// Prevent accidentally mixing types that are structurally identical
type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };
function createUserId(id: string): UserId { return id as UserId; }
function getUser(id: UserId) { /* ... */ }
const userId = createUserId("u-123");
const orderId = "o-456" as OrderId;
getUser(userId); // OK
getUser(orderId); // Error: OrderId not assignable to UserId
Runtime Validation (Zod 4)
import { z } from "zod";
// Define schema (Zod 4: string formats are top-level - z.email(), not z.string().email())
const UserSchema = z.object({
id: z.number(),
name: z.string().min(1),
email: z.email(),
role: z.enum(["admin", "user"]),
settings: z.object({
theme: z.enum(["light", "dark"]).default("light"),
}).optional(),
});
// Infer type from schema
type User = z.infer<typeof UserSchema>;
// Validate
const user = UserSchema.parse(untrustedData); // throws on invalid
const result = UserSchema.safeParse(untrustedData); // returns { success, data/error }
Zod 4 changes to know (if you learned Zod 3): string formats moved to the top level
(z.email(), z.uuid(), z.url() — the z.string().email() method form is deprecated);
error customisation unified under a single error param (invalid_type_error /
required_error dropped); much faster parsing and a tree-shakeable zod/mini entry point.
Reference Files
Load these for deep-dive topics. Each is self-contained.
| Reference | When to Load |
|---|---|
./references/type-system.md |
Advanced types, branded types, type-level programming, satisfies operator |
./references/generics-patterns.md |
Generic constraints, conditional types, mapped types, template literals, recursive types |
./references/utility-types.md |
All built-in utility types with examples, custom utility types |
./references/config-strict.md |
tsconfig deep dive, strict mode migration, project references, monorepo setup |
./references/ts7-native-compiler.md |
Adopting the TS 7 native (Go) compiler: no JS API, bin ambiguity, ecosystem lockout, benchmarked go/no-go |
./references/ecosystem.md |
Zod/Valibot, type-safe API clients, ORM types, testing with Vitest |
See Also
testing-ops- Cross-language testing strategiesci-cd-ops- TypeScript CI pipelines, type checking in CI
Files (claude-mods)
-
assets
-
typescript-facts.json 1.7 KB
{ "_comment": "Canonical fast-moving facts the typescript-ops skill encodes. scripts/check-typescript-facts.py asserts SKILL.md + references name these consistently (--offline) and probes the npm registry for major-version drift (--live). Edit deliberately: a change here is a skill-content decision, not housekeeping.", "schema": "claude-mods.typescript-ops.facts/v1", "as_of": "2026-08-08", "registry": "https://registry.npmjs.org", "currency_note": "> Ecosystem facts verified as of 2026-08-08 (TypeScript 7, Zod 4, Valibot 1).", "packages": [ { "name": "typescript", "documented_major": 7, "role": "language", "where": "SKILL.md (Modern Language Features 5.x -> 6.0: satisfies, standard decorators, const type params, using, inferred predicates; TS 6.0 bridge section; TS 7 native-compiler section); references/ts7-native-compiler.md (no JS API on 7.0.x, dual-install bin ambiguity, ecosystem lockout until 7.1, benchmarked adoption); references/config-strict.md (strict default in 6.0, removed module/moduleResolution options). Historical feature floors (TS 4.4+, 4.7+) remain as floors, not currency anchors." }, { "name": "zod", "documented_major": 4, "role": "runtime validation (default choice)", "where": "SKILL.md (Runtime Validation (Zod 4): top-level z.email(), unified error param, zod/mini); references/ecosystem.md (z.uuid/z.email top-level formats, unified error param, discriminatedUnion, coerce)." }, { "name": "valibot", "documented_major": 1, "role": "runtime validation (tree-shakeable alt)", "where": "references/ecosystem.md (stable since 1.0; v.pipe, v.InferOutput, v.safeParse, v.picklist)." } ] }
-
-
references
-
config-strict.md 13.4 KB
# TypeScript Configuration and Strict Mode Reference ## Table of Contents 1. [Strict Mode Flags](#strict-mode-flags) 2. [Migration Strategy](#migration-strategy) 3. [Module Configuration](#module-configuration) 4. [Path Aliases](#path-aliases) 5. [Project References](#project-references) 6. [Monorepo Setup](#monorepo-setup) 7. [Declaration Files](#declaration-files) --- ## Strict Mode Flags ### Enable the Full Strict Suite `"strict": true` is shorthand for enabling all individual strict flags at once. Always enable it. Since TypeScript 6.0 it is the **default** — a tsconfig that never set `strict` now gets the full suite, so a project upgrading to TS 6 may surface long-hidden errors; set `"strict": false` only as a deliberate, temporary migration step. ```json { "compilerOptions": { "strict": true, // Additional strictness beyond "strict": true "noUncheckedIndexedAccess": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "exactOptionalPropertyTypes": true, "noPropertyAccessFromIndexSignature": true, "noImplicitOverride": true } } ``` ### Understand Each Flag **strictNullChecks** - `null` and `undefined` are not assignable to other types. The most impactful flag. ```typescript // Without strictNullChecks: null assignable to anything // With strictNullChecks: function getLength(s: string): number { return s.length; // OK } getLength(null); // Error: Argument of type 'null' is not assignable to parameter of type 'string' // Forces explicit null handling: function getName(user: { name: string } | null): string { return user?.name ?? 'Anonymous'; } ``` **strictFunctionTypes** - Function parameters are checked contravariantly, not bivariantly. ```typescript type Animal = { name: string }; type Dog = Animal & { breed: string }; type AnimalCallback = (a: Animal) => void; type DogCallback = (d: Dog) => void; let animalCb: AnimalCallback = (a) => console.log(a.name); let dogCb: DogCallback = (d) => console.log(d.breed); // With strictFunctionTypes, this is an error (unsafe in callback position): // dogCb = animalCb; // DogCallback expects d.breed but AnimalCallback only provides a.name ``` **strictBindCallApply** - `.bind()`, `.call()`, `.apply()` are type-checked. ```typescript function add(a: number, b: number): number { return a + b; } add.call(null, 1, 2); // OK add.call(null, '1', 2); // Error: Argument of type 'string' not assignable to 'number' add.bind(null, 1)(2); // OK, typed as () => number after bind ``` **strictPropertyInitialization** - Class properties must be assigned in the constructor. ```typescript class Service { name: string; // Error: not definitely assigned id: string; // Error: not definitely assigned // Fix options: optA: string = ''; // default value optB!: string; // definite assignment assertion (use sparingly) optC: string | undefined; // allow undefined constructor() { this.optA = this.optA; } // assign in constructor } ``` **noImplicitAny** - Variables whose type cannot be inferred default to `any` - this flag makes that an error. ```typescript function process(data) { // Error: 'data' implicitly has an 'any' type return data.value; } function process(data: { value: string }): string { // OK return data.value; } ``` **noImplicitThis** - `this` usage without explicit annotation is an error. ```typescript function greet() { return this.name; // Error: 'this' implicitly has type 'any' } function greet(this: { name: string }): string { return this.name; // OK - this is typed } ``` **useUnknownInCatchVariables** (part of strict in TS 4.4+) - Catch clause variables are `unknown`, not `any`. ```typescript try { riskyOperation(); } catch (err) { // err is 'unknown' - must narrow before use if (err instanceof Error) { console.error(err.message); // OK } else { console.error(String(err)); // handle non-Error throws } } ``` **noUncheckedIndexedAccess** - Index signatures include `undefined` in return type. ```typescript const map: Record<string, string> = {}; const value = map['key']; // string | undefined (not just string) // Forces null checking: if (value !== undefined) { console.log(value.toUpperCase()); // OK } ``` **exactOptionalPropertyTypes** - Distinguishes between `prop?: T` (absent or T) and `prop: T | undefined`. ```typescript interface A { name?: string; } // With exactOptionalPropertyTypes: const a: A = { name: undefined }; // Error: undefined is not the same as absent const b: A = {}; // OK - name is absent const c: A = { name: 'Alice' }; // OK ``` --- ## Migration Strategy ### Adopt Strict Mode Incrementally ```json // Phase 1: Start here - catches the worst issues { "compilerOptions": { "noImplicitAny": true, "strictNullChecks": true } } // Phase 2: Add remaining strict flags { "compilerOptions": { "strict": true } } // Phase 3: Tighten further { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noImplicitReturns": true } } ``` ### Use @ts-expect-error for Tracked Suppressions Prefer `@ts-expect-error` over `@ts-ignore`. The former causes a type error if the suppressed line no longer has an error, making it self-cleaning. ```typescript // @ts-ignore - silently does nothing if the error is later fixed (dead suppression) const x: string = 42; // @ts-expect-error - causes a type error when the suppressed error is fixed // @ts-expect-error: temporary until API is updated const y: string = legacyApi.getValue(); ``` ### Migration Checklist ``` [ ] Enable noImplicitAny first - forces all untyped code to be explicit [ ] Add @ts-expect-error to suppress errors in files not yet migrated [ ] Enable strictNullChecks - fix null/undefined handling [ ] Enable strict: true - address remaining flags [ ] Track suppressions with: grep -r "@ts-expect-error" . --include="*.ts" [ ] Eliminate suppressions file by file [ ] Enable noUncheckedIndexedAccess as final step (highest refactor cost) ``` --- ## Module Configuration ### Choose the Right module and moduleResolution ```json // For Node.js with CommonJS { "compilerOptions": { "module": "CommonJS", "moduleResolution": "Node" } } // For Node.js with ESM (Node 18+) - recommended for new Node projects { "compilerOptions": { "module": "Node16", // or "NodeNext" "moduleResolution": "Node16" } } // For bundlers (Vite, webpack, esbuild, Rollup) { "compilerOptions": { "module": "ESNext", "moduleResolution": "Bundler" } } ``` **TypeScript 6.0 removals**: `moduleResolution: "Classic"` and `module: "amd" / "umd" / "system" / "none"` were removed outright, and `esModuleInterop` / `allowSyntheticDefaultImports` can no longer be set to `false` (the safe interop behaviour is always on). Defaults are now `module: "esnext"` and `target: "es2025"`; the minimum `target` is ES2015. If a legacy tsconfig names any removed option, migrate to `Bundler` or `Node16`/`NodeNext` before upgrading. ### Understand ESM vs CJS Interop Issues With `Node16`/`NodeNext`, you must use explicit `.js` extensions in relative imports (even for `.ts` files). ```typescript // tsconfig.json: "module": "Node16" // WRONG - no extension import { helper } from './helper'; // CORRECT - use .js extension (TypeScript resolves it to .ts) import { helper } from './helper.js'; ``` Set `"type": "module"` in `package.json` to use ESM, or use `.mts`/`.cts` file extensions to override per-file. ```json // package.json { "type": "module" } ``` --- ## Path Aliases ### Configure Paths in tsconfig.json Write `paths` entries tsconfig-relative (`./src/*`) and omit `baseUrl` entirely: it has been unnecessary for `paths` since TS 4.1, and TypeScript 7 hard-errors on it (TS5102 — see `ts7-native-compiler.md`). ```json { "compilerOptions": { "paths": { "@/*": ["./src/*"], "@components/*": ["./src/components/*"], "@utils/*": ["./src/utils/*"], "@types/*": ["./src/types/*"] } } } ``` ### Use Paths with Vite ```typescript // vite.config.ts import { defineConfig } from 'vite'; import { resolve } from 'path'; export default defineConfig({ resolve: { alias: { '@': resolve(__dirname, './src'), '@components': resolve(__dirname, './src/components'), '@utils': resolve(__dirname, './src/utils'), }, }, }); ``` ### Use Paths with Node (tsx / tsconfig-paths) ```bash # Option 1: tsx (recommended for scripts/CLIs) npx tsx --tsconfig tsconfig.json src/index.ts # Option 2: tsconfig-paths with ts-node npx ts-node -r tsconfig-paths/register src/index.ts # Option 3: tsconfig-paths at runtime (after compilation) node -r tsconfig-paths/register dist/index.js ``` ```typescript // tsconfig-paths at runtime setup // bootstrap.js const { register } = require('tsconfig-paths'); const tsConfig = require('./tsconfig.json'); register({ // tsconfig has no baseUrl (TS 7 errors on it) — anchor at the config's own dir baseUrl: __dirname, paths: tsConfig.compilerOptions.paths, }); require('./dist/index.js'); ``` --- ## Project References ### Set Up Composite Projects Project references allow incremental builds and better IDE performance in large repos. ```json // packages/shared/tsconfig.json { "compilerOptions": { "composite": true, // required for project references "declaration": true, // required for project references "declarationMap": true, "outDir": "./dist", "rootDir": "./src" } } // packages/app/tsconfig.json { "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "references": [ { "path": "../shared" } ] } ``` ### Build with --build Mode ```bash # Build all referenced projects in dependency order tsc --build # Build and watch tsc --build --watch # Clean built outputs tsc --build --clean # Force rebuild tsc --build --force ``` --- ## Monorepo Setup ### Define a Root tsconfig for Shared Settings ```json // tsconfig.base.json (root) { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "noImplicitReturns": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "declaration": true, "declarationMap": true, "sourceMap": true } } // packages/server/tsconfig.json { "extends": "../../tsconfig.base.json", "compilerOptions": { "module": "CommonJS", "moduleResolution": "Node", "target": "ES2022", "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"], "references": [{ "path": "../shared" }] } // packages/web/tsconfig.json { "extends": "../../tsconfig.base.json", "compilerOptions": { "module": "ESNext", "moduleResolution": "Bundler", "target": "ES2022", "jsx": "react-jsx", "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"], "references": [{ "path": "../shared" }] } ``` ### Use a Root tsconfig for IDE Support ```json // tsconfig.json (root - IDE only, not for building) { "files": [], "references": [ { "path": "./packages/shared" }, { "path": "./packages/server" }, { "path": "./packages/web" } ] } ``` --- ## Declaration Files ### Write Ambient Declarations for Untyped Modules ```typescript // types/untyped-module.d.ts declare module 'some-legacy-package' { export interface Options { timeout?: number; retries?: number; } export function connect(url: string, options?: Options): Promise<void>; export function disconnect(): void; export default { connect, disconnect, }; } // Wildcard module for assets (e.g., CSS, SVG) declare module '*.svg' { const content: string; export default content; } declare module '*.png' { const content: string; export default content; } declare module '*.css' { const styles: Record<string, string>; export default styles; } ``` ### Augment Global Scope ```typescript // global.d.ts declare global { // Extend the Window interface interface Window { __APP_VERSION__: string; analytics: { track(event: string, props?: Record<string, unknown>): void; }; } // Extend ProcessEnv for typed environment variables namespace NodeJS { interface ProcessEnv { NODE_ENV: 'development' | 'production' | 'test'; DATABASE_URL: string; API_KEY: string; PORT?: string; } } } export {}; // This export makes the file a module, enabling declare global ``` ### Use Triple-Slash Directives ```typescript // Reference a type definition file /// <reference types="node" /> /// <reference types="jest" /> // Reference a specific .d.ts file /// <reference path="../types/custom.d.ts" /> // Reference a lib /// <reference lib="dom" /> /// <reference lib="es2022" /> ``` ### Write a .d.ts for a Hand-Authored JavaScript Library ```typescript // src/math-helpers.js (source) function add(a, b) { return a + b; } function multiply(a, b) { return a * b; } module.exports = { add, multiply }; // src/math-helpers.d.ts (declaration) export declare function add(a: number, b: number): number; export declare function multiply(a: number, b: number): number; ``` ### Configure Declaration Output ```json { "compilerOptions": { "declaration": true, // emit .d.ts files "declarationDir": "./types", // output directory for .d.ts (optional) "declarationMap": true, // emit .d.ts.map for source navigation "emitDeclarationOnly": true // only emit .d.ts, no JS (when bundler handles JS) } } ``` -
ecosystem.md 17.1 KB
# TypeScript Ecosystem Reference ## Table of Contents 1. [Runtime Validation](#runtime-validation) 2. [Type-Safe API Clients](#type-safe-api-clients) 3. [ORM Types](#orm-types) 4. [Testing with Types](#testing-with-types) 5. [Type-Safe Routing](#type-safe-routing) 6. [Effect](#effect) 7. [ts-pattern](#ts-pattern) 8. [Type Challenges](#type-challenges) --- ## Runtime Validation ### Use Zod for Schema Validation with Type Inference Zod is the most widely adopted runtime validation library. Define a schema once; infer the TypeScript type from it. As of Zod 4, string formats are top-level functions (`z.email()`, `z.uuid()`, `z.url()`) — the Zod 3 method forms (`z.string().email()`) are deprecated. ```typescript import { z } from 'zod'; // Define schema (Zod 4 syntax) const UserSchema = z.object({ id: z.uuid(), name: z.string().min(1).max(100), email: z.email(), age: z.number().int().min(0).max(150).optional(), role: z.enum(['admin', 'user', 'moderator']), createdAt: z.coerce.date(), }); // Infer TypeScript type from schema - single source of truth type User = z.infer<typeof UserSchema>; // { id: string; name: string; email: string; age?: number; role: 'admin' | 'user' | 'moderator'; createdAt: Date } // Parse and validate (throws ZodError on failure) const user = UserSchema.parse(rawData); // Safe parse (returns success/failure object, never throws) const result = UserSchema.safeParse(rawData); if (result.success) { console.log(result.data); // typed as User } else { console.error(result.error.flatten()); // ZodError with friendly message structure } ``` ### Apply Zod Transforms and Refinements ```typescript const PasswordSchema = z .string() .min(8, 'Password must be at least 8 characters') .regex(/[A-Z]/, 'Password must contain an uppercase letter') .regex(/[0-9]/, 'Password must contain a number'); // Transform: parse then convert const DateStringSchema = z.string().transform((s) => new Date(s)); type DateValue = z.infer<typeof DateStringSchema>; // Date (output type after transform) // Input type is string; output type is Date // Refine: validate with custom logic const EvenNumberSchema = z.number().refine( (n) => n % 2 === 0, { message: 'Number must be even' } ); // Custom error messages (Zod 4: one unified `error` param replaces // invalid_type_error / required_error / errorMap) const AgeSchema = z.number({ error: 'Age must be a number' }).int().min(0); // Discriminated union (Zod version) const ApiResponseSchema = z.discriminatedUnion('status', [ z.object({ status: z.literal('success'), data: z.unknown() }), z.object({ status: z.literal('error'), message: z.string() }), ]); type ApiResponse = z.infer<typeof ApiResponseSchema>; ``` ### Use Valibot as a Tree-Shakeable Alternative Valibot (stable since 1.0) covers the same ground as Zod but is tree-shakeable by design, resulting in much smaller bundles for edge/browser deployments. Zod 4's `zod/mini` entry point narrows the gap, but valibot's pipe composition remains the smallest-bundle option. ```typescript import * as v from 'valibot'; const UserSchema = v.object({ id: v.pipe(v.string(), v.uuid()), name: v.pipe(v.string(), v.minLength(1), v.maxLength(100)), email: v.pipe(v.string(), v.email()), role: v.picklist(['admin', 'user', 'moderator']), }); type User = v.InferOutput<typeof UserSchema>; const result = v.safeParse(UserSchema, rawData); if (result.success) { console.log(result.output); // typed as User } ``` ### Compare Zod vs Valibot | Concern | Zod | Valibot | |---------|-----|---------| | Bundle size | ~13 kB min+gz | ~0.5-2 kB (tree-shaken) | | API style | Method chaining | Pipe/function composition | | Ecosystem | Larger (more integrations) | Smaller but growing | | Best for | Node.js / full-stack | Edge / browser | | Async validation | `z.refine(async ...)` | `v.pipeAsync(...)` | --- ## Type-Safe API Clients ### Build a Type-Safe Fetch Wrapper ```typescript type ApiResponse<T> = | { ok: true; data: T; status: number } | { ok: false; error: string; status: number }; async function apiFetch<T>( url: string, schema: { parse: (data: unknown) => T }, init?: RequestInit ): Promise<ApiResponse<T>> { try { const response = await fetch(url, init); const json: unknown = await response.json(); if (!response.ok) { return { ok: false, error: String(json), status: response.status }; } const data = schema.parse(json); return { ok: true, data, status: response.status }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : 'Unknown error', status: 0 }; } } // Usage with Zod schema const UsersSchema = z.array(UserSchema); const result = await apiFetch('/api/users', UsersSchema); if (result.ok) { result.data; // User[] } ``` ### Use openapi-typescript for Contract-First APIs ```bash # Generate TypeScript types from an OpenAPI spec npx openapi-typescript ./openapi.yaml -o ./src/types/api.d.ts # or from a URL npx openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.d.ts ``` ```typescript import type { paths, components } from './types/api.d.ts'; // Use generated types in a typed client type GetUserParams = paths['/users/{id}']['get']['parameters']; type GetUserResponse = paths['/users/{id}']['get']['responses']['200']['content']['application/json']; type User = components['schemas']['User']; ``` ### Add tRPC for End-to-End Type Safety ```typescript // server/router.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ user: t.router({ getById: t.procedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { return await db.user.findUnique({ where: { id: input.id } }); }), create: t.procedure .input(z.object({ name: z.string(), email: z.email() })) .mutation(async ({ input }) => { return await db.user.create({ data: input }); }), }), }); export type AppRouter = typeof appRouter; // client/trpc.ts import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from '../server/router'; const trpc = createTRPCProxyClient<AppRouter>({ links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })], }); // Fully type-safe - input and output types inferred from router const user = await trpc.user.getById.query({ id: '123' }); // user is typed as the return type of the resolver - no manual typing needed ``` --- ## ORM Types ### Generate Types with Prisma Prisma generates complete TypeScript types from the schema file. ```prisma // prisma/schema.prisma model User { id String @id @default(cuid()) email String @unique name String? posts Post[] createdAt DateTime @default(now()) } ``` ```typescript import { PrismaClient } from '@prisma/client'; // Prisma generates: // - PrismaClient with typed query methods // - User, Post, etc. model types // - UserCreateInput, UserUpdateInput, UserWhereInput, etc. const db = new PrismaClient(); // Fully typed queries const user = await db.user.findUniqueOrThrow({ where: { email: 'alice@example.com' }, include: { posts: true }, }); // user: User & { posts: Post[] } // Use generated input types import type { Prisma } from '@prisma/client'; async function createUser(data: Prisma.UserCreateInput) { return db.user.create({ data }); } // Select subsets for performance type UserPreview = Prisma.UserGetPayload<{ select: { id: true; name: true; email: true }; }>; ``` ### Write Type-Safe Queries with Drizzle ORM Drizzle is a SQL-first ORM where types flow from the schema definition. ```typescript import { pgTable, text, integer, timestamp } from 'drizzle-orm/pg-core'; import { drizzle } from 'drizzle-orm/node-postgres'; import { eq } from 'drizzle-orm'; const users = pgTable('users', { id: text('id').primaryKey(), name: text('name').notNull(), email: text('email').notNull().unique(), age: integer('age'), createdAt: timestamp('created_at').defaultNow(), }); // Infer types directly from table definition type User = typeof users.$inferSelect; // for SELECT results type NewUser = typeof users.$inferInsert; // for INSERT data const db = drizzle(pool); // Type-safe queries - IDE autocomplete on column names const allUsers = await db.select().from(users); // allUsers: User[] const alice = await db.select().from(users).where(eq(users.email, 'alice@example.com')); // alice: User[] await db.insert(users).values({ id: '1', name: 'Alice', email: 'alice@example.com' }); // Type error if required fields are missing ``` ### Query with Kysely for SQL-First Type Safety Kysely provides type-safe query building without code generation. ```typescript import { Kysely, PostgresDialect } from 'kysely'; interface Database { users: { id: string; name: string; email: string; age: number | null }; posts: { id: string; userId: string; title: string; content: string }; } const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool }) }); const users = await db .selectFrom('users') .select(['id', 'name', 'email']) .where('age', '>', 18) .execute(); // users: Array<{ id: string; name: string; email: string }> ``` --- ## Testing with Types ### Use expectTypeOf in Vitest ```typescript import { expectTypeOf, test } from 'vitest'; test('identity function preserves type', () => { function identity<T>(value: T): T { return value; } expectTypeOf(identity('hello')).toEqualTypeOf<string>(); expectTypeOf(identity(42)).toEqualTypeOf<number>(); expectTypeOf(identity).toBeFunction(); expectTypeOf(identity).parameter(0).toBeString(); }); test('Result type narrows correctly', () => { type Result<T> = { ok: true; value: T } | { ok: false; error: string }; function ok<T>(value: T): Result<T> { return { ok: true, value }; } function err<T>(error: string): Result<T> { return { ok: false, error }; } expectTypeOf(ok('data')).toEqualTypeOf<Result<string>>(); expectTypeOf(err<number>('oops')).toEqualTypeOf<Result<number>>(); }); ``` ### Use assertType for Compile-Time Checks ```typescript import { assertType, test } from 'vitest'; test('types are correct', () => { // assertType<T>(value) asserts value matches type T at compile time // (no runtime effect - type-only check) assertType<string>('hello'); assertType<number>(42); // @ts-expect-error assertions that should fail // @ts-expect-error assertType<string>(42); // fails: 42 is not string }); ``` ### Use tsd for Testing Declaration Files `tsd` is dedicated to testing `.d.ts` files. It checks that type definitions behave correctly. ```typescript // index.test-d.ts import { expectType, expectError, expectAssignable } from 'tsd'; import { getUser, createUser } from './index.js'; // Check return types expectType<Promise<User>>(getUser('123')); // Check that invalid calls produce errors expectError(getUser(123)); // Error: number not assignable to string // Check assignability (less strict than equality) expectAssignable<{ id: string }>(await getUser('1')); ``` ```json // package.json { "scripts": { "test:types": "tsd" }, "tsd": { "directory": "test" } } ``` --- ## Type-Safe Routing ### Use Next.js Typed Routes Next.js 13+ supports experimental typed routes that validate `href` values. ```json // next.config.js { "experimental": { "typedRoutes": true } } ``` ```typescript import Link from 'next/link'; // TypeScript validates the href against your actual routes <Link href="/about">About</Link> // OK if /about exists <Link href="/users/[id]">User</Link> // Error: must pass actual id <Link href={{ pathname: '/users/[id]', params: { id: '1' } }}>User</Link> // OK ``` ### Build Type-Safe Path Parameters ```typescript // Generic route parameter extractor type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param]: string } & ExtractParams<Rest> : T extends `${string}:${infer Param}` ? { [K in Param]: string } : Record<string, never>; type Prettify<T> = { [K in keyof T]: T[K] } & {}; function createRoute<T extends string>( template: T ): { path: T; build(params: Prettify<ExtractParams<T>>): string } { return { path: template, build(params) { return Object.entries(params).reduce( (path, [key, value]) => path.replace(`:${key}`, value as string), template ); }, }; } const userRoute = createRoute('/users/:userId/posts/:postId'); const url = userRoute.build({ userId: '1', postId: '42' }); // '/users/1/posts/42' // TypeScript error if userId or postId is missing ``` --- ## Effect ### Use Effect-TS for Typed Functional Error Handling Effect models computations as `Effect<Value, Error, Requirements>`. Errors are part of the type, not thrown. ```typescript import { Effect, pipe } from 'effect'; // Define typed errors class UserNotFoundError { readonly _tag = 'UserNotFoundError'; constructor(readonly id: string) {} } class DatabaseError { readonly _tag = 'DatabaseError'; constructor(readonly message: string) {} } // Effect<User, UserNotFoundError | DatabaseError, never> // Value: User, Error: UserNotFoundError | DatabaseError, Requirements: none const getUser = (id: string): Effect.Effect<User, UserNotFoundError | DatabaseError> => Effect.tryPromise({ try: () => db.user.findUniqueOrThrow({ where: { id } }), catch: (e) => e instanceof Error && e.message.includes('No User found') ? new UserNotFoundError(id) : new DatabaseError(String(e)), }); // Compose effects with pipe const program = pipe( getUser('123'), Effect.map((user) => user.name), Effect.catchTag('UserNotFoundError', (e) => Effect.succeed(`User ${e.id} not found`) ), // DatabaseError is still in the error channel - must be handled or propagated ); // Run the effect const result = await Effect.runPromise(program); ``` --- ## ts-pattern ### Match Exhaustively with ts-pattern `ts-pattern` provides pattern matching with full TypeScript type narrowing. ```typescript import { match, P } from 'ts-pattern'; type ApiState = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: User[] } | { status: 'error'; error: Error }; function render(state: ApiState): string { return match(state) .with({ status: 'idle' }, () => 'Ready') .with({ status: 'loading' }, () => 'Loading...') .with({ status: 'success', data: P.select() }, (data) => `Loaded ${data.length} users` ) .with({ status: 'error', error: P.select() }, (error) => `Error: ${error.message}` ) .exhaustive(); // Compile error if a variant is unhandled } // Pattern guards const result = match(value) .with(P.number.gt(100), (n) => `Big: ${n}`) .with(P.number.lt(0), (n) => `Negative: ${n}`) .with(P.number, (n) => `Normal: ${n}`) .with(P.string, (s) => `String: ${s}`) .otherwise(() => 'Unknown'); // Nested matching const message = match(response) .with({ type: 'error', code: P.union(401, 403) }, () => 'Unauthorized') .with({ type: 'error', code: 404 }, () => 'Not Found') .with({ type: 'error' }, () => 'Server Error') .with({ type: 'success' }, () => 'OK') .exhaustive(); ``` --- ## Type Challenges ### Practice Advanced Types Effectively The `type-challenges` repository (github.com/type-challenges/type-challenges) provides 200+ graded exercises. ```typescript // Example: Implement Readonly<T> from scratch type MyReadonly<T> = { readonly [K in keyof T]: T[K]; }; // Example: Implement Pick<T, K> type MyPick<T, K extends keyof T> = { [P in K]: T[P]; }; // Example: Implement Exclude<T, U> type MyExclude<T, U> = T extends U ? never : T; // Example: Implement ReturnType<T> type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never; // Example: Deep Readonly type DeepReadonly<T> = keyof T extends never ? T : { readonly [K in keyof T]: DeepReadonly<T[K]> }; ``` ### Use the TypeScript Playground The TypeScript Playground (typescriptlang.org/play) supports: - Sharing type puzzles via URL - Viewing emitted JavaScript - Checking against multiple TS versions - Running code in browser ### Recommended Learning Resources | Resource | Focus | |----------|-------| | `type-challenges` on GitHub | Exercises from easy to extreme | | Matt Pocock's Total TypeScript | Tutorials and workshops | | TypeScript Deep Dive (basarat) | Comprehensive free book | | Official TS Handbook | Language reference | | tsdocs.dev | Browse type definitions for any npm package | | typescript-eslint.io | Type-aware lint rules | ### Set Up a Type Testing Playground Locally ```bash mkdir ts-playground && cd ts-playground npm init -y npm install -D typescript tsx @types/node cat > tsconfig.json << 'EOF' { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler" } } EOF # Write type experiments cat > playground.ts << 'EOF' type Test = /* your type here */; type Expect<T extends true> = T; type Equal<A, B> = A extends B ? B extends A ? true : false : false; type Case1 = Expect<Equal<Test, ExpectedType>>; EOF npx tsx playground.ts ``` -
generics-patterns.md 14.6 KB
# TypeScript Generics Patterns Reference ## Table of Contents 1. [Generic Functions](#generic-functions) 2. [Generic Classes](#generic-classes) 3. [Generic Interfaces](#generic-interfaces) 4. [Conditional Types](#conditional-types) 5. [Mapped Types](#mapped-types) 6. [Template Literal Types in Generics](#template-literal-types-in-generics) 7. [Variadic Tuple Types](#variadic-tuple-types) 8. [Higher-Kinded Types](#higher-kinded-types) 9. [Builder Pattern](#builder-pattern) 10. [Common Generic Patterns](#common-generic-patterns) --- ## Generic Functions ### Infer Type Parameters From Arguments TypeScript infers type parameters from call-site arguments. Prefer inference over explicit type args. ```typescript // Inferred: T = string from the argument function identity<T>(value: T): T { return value; } const s = identity('hello'); // T inferred as string // Constraint: 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('alice', 'bob'); // OK - strings have length longest([1, 2, 3], [1, 2]); // OK - arrays have length longest({ length: 5 }, { length: 3 }); // OK // Multiple type parameters with relationship constraint function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user = { id: 1, name: 'Alice', active: true }; getProperty(user, 'name'); // string getProperty(user, 'active'); // boolean getProperty(user, 'foo'); // Error: 'foo' is not a key of typeof user ``` ### Use Default Type Parameters ```typescript // Default type parameter when not specified function createArray<T = string>(length: number, fill: T): T[] { return Array(length).fill(fill); } const strings = createArray(3, 'x'); // string[] - T inferred const numbers = createArray(3, 0); // number[] - T inferred const explicit = createArray<boolean>(3, true); // boolean[] // Useful in generic components/hooks interface PaginatedResponse<T = unknown> { data: T[]; total: number; page: number; } ``` --- ## Generic Classes ### Parameterize Class Behavior ```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.pop(); // number | undefined ``` ### Recognize the Static Members Limitation Static members cannot reference a class's type parameters. The type parameter belongs to an instance. ```typescript class Container<T> { value: T; // OK - instance member constructor(value: T) { this.value = value; } // static defaultValue: T; // Error: static members can't reference type parameters // Workaround: use a separate factory type or factory method static create<U>(value: U): Container<U> { return new Container(value); } } ``` --- ## Generic Interfaces ### Implement the Repository Pattern ```typescript interface Repository<T, ID = string> { findById(id: ID): Promise<T | null>; findAll(filter?: Partial<T>): Promise<T[]>; save(entity: T): Promise<T>; delete(id: ID): Promise<void>; } interface User { id: string; name: string; email: string; } class UserRepository implements Repository<User, string> { async findById(id: string): Promise<User | null> { /* ... */ return null; } async findAll(filter?: Partial<User>): Promise<User[]> { /* ... */ return []; } async save(entity: User): Promise<User> { /* ... */ return entity; } async delete(id: string): Promise<void> { /* ... */ } } ``` ### Implement the Factory Pattern ```typescript interface Factory<T, TArgs extends unknown[] = []> { create(...args: TArgs): T; } class ConnectionFactory implements Factory<Connection, [string, number]> { create(host: string, port: number): Connection { return new Connection(host, port); } } ``` --- ## Conditional Types ### Distribute Over Union Types Conditional types distribute over naked type parameters in unions. ```typescript // Distributes: IsString<string | number> = IsString<string> | IsString<number> type IsString<T> = T extends string ? true : false; type Test = IsString<string | number>; // true | false = boolean // To prevent distribution, wrap in a tuple type IsStringExact<T> = [T] extends [string] ? true : false; type Test2 = IsStringExact<string | number>; // false ``` ### Use infer to Extract Types ```typescript // Extract the element type from an array type UnpackArray<T> = T extends (infer U)[] ? U : T; type Item = UnpackArray<string[]>; // string type Same = UnpackArray<number>; // number // Extract return type (equivalent to built-in ReturnType) type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never; // Extract the resolved value of a Promise type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T; type Value = Awaited<Promise<Promise<string>>>; // string // Extract first parameter type type FirstParam<T> = T extends (first: infer F, ...rest: unknown[]) => unknown ? F : never; type F = FirstParam<(a: string, b: number) => void>; // string // Extract constructor instance type type InstanceOf<T> = T extends new (...args: unknown[]) => infer I ? I : never; ``` ### Nest Conditional Types for Complex Logic ```typescript type TypeName<T> = T extends string ? 'string' : T extends number ? 'number' : T extends boolean ? 'boolean' : T extends null ? 'null' : T extends undefined ? 'undefined' : T extends Function ? 'function' : 'object'; type A = TypeName<string>; // 'string' type B = TypeName<() => void>; // 'function' type C = TypeName<{ a: 1 }>; // 'object' // Filter a union: keep only string keys from a type type StringKeys<T> = { [K in keyof T]: T[K] extends string ? K : never; }[keyof T]; interface Mixed { id: string; count: number; name: string; active: boolean; } type OnlyStringFields = StringKeys<Mixed>; // 'id' | 'name' ``` --- ## Mapped Types ### Remap Keys with the as Clause ```typescript // Prefix all keys type Prefixed<T, P extends string> = { [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K]; }; interface User { id: string; name: string; } type PrefixedUser = Prefixed<User, 'user'>; // { userId: string; userName: string } // Filter keys by value type type PickByValue<T, V> = { [K in keyof T as T[K] extends V ? K : never]: T[K]; }; interface Config { debug: boolean; port: number; host: string; verbose: boolean; } type BooleanConfig = PickByValue<Config, boolean>; // { debug: boolean; verbose: boolean } ``` ### Apply Modifiers with + and - ```typescript // Add readonly and optional type Immutable<T> = { +readonly [K in keyof T]+?: T[K]; }; // Remove readonly and optional (make mutable and required) type Mutable<T> = { -readonly [K in keyof T]-?: T[K]; }; interface Optional { readonly id?: string; readonly name?: string; } type Concrete = Mutable<Optional>; // { id: string; name: string } ``` ### Combine Mapped and Conditional Types ```typescript // Make only specific keys optional type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; interface Post { id: string; title: string; content: string; publishedAt: Date; } type DraftPost = MakeOptional<Post, 'id' | 'publishedAt'>; // { title: string; content: string; id?: string; publishedAt?: Date } ``` --- ## Template Literal Types in Generics ### Build Type-Safe Event Emitters ```typescript type EventMap = { userCreated: { userId: string }; orderPlaced: { orderId: string; total: number }; sessionExpired: { sessionId: string }; }; type EventListener<TMap, TEvent extends keyof TMap> = (event: TMap[TEvent]) => void; type Emitter<TMap> = { on<TEvent extends keyof TMap>( event: TEvent, listener: EventListener<TMap, TEvent> ): void; emit<TEvent extends keyof TMap>(event: TEvent, data: TMap[TEvent]): void; }; declare const emitter: Emitter<EventMap>; emitter.on('userCreated', (e) => console.log(e.userId)); // OK emitter.on('orderPlaced', (e) => console.log(e.total)); // OK emitter.emit('userCreated', { userId: '123' }); // OK emitter.emit('userCreated', { orderId: '123' }); // Error: wrong shape ``` ### Extract Route Parameters ```typescript type RouteParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param | keyof RouteParams<Rest>]: string } : T extends `${string}:${infer Param}` ? { [K in Param]: string } : Record<string, never>; function buildRoute<T extends string>( template: T, params: RouteParams<T> ): string { return Object.entries(params).reduce( (path, [key, value]) => path.replace(`:${key}`, value as string), template ); } const url = buildRoute('/users/:userId/posts/:postId', { userId: '1', postId: '42', }); // '/users/1/posts/42' ``` --- ## Variadic Tuple Types ### Spread Tuples for Function Composition ```typescript // Concatenate two tuple types type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U]; type T1 = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4] // Strongly typed pipe/compose type Pipe<T extends ((...args: unknown[]) => unknown)[]> = T extends [infer First, ...infer Rest] ? First extends (...args: infer A) => infer R ? Rest extends [] ? (...args: A) => R : Pipe<[(...args: A) => R, ...Extract<Rest, ((...args: unknown[]) => unknown)[]>]> : never : never; // Prepend and append to tuples type Prepend<T, Tuple extends unknown[]> = [T, ...Tuple]; type Append<Tuple extends unknown[], T> = [...Tuple, T]; type WithFirst = Prepend<string, [number, boolean]>; // [string, number, boolean] type WithLast = Append<[string, number], boolean>; // [string, number, boolean] ``` ### Build Type-Safe curry ```typescript type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never; type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : never; type Curry<TArgs extends unknown[], TReturn> = TArgs extends [] ? TReturn : (arg: Head<TArgs>) => Curry<Tail<TArgs>, TReturn>; declare function curry<TArgs extends unknown[], TReturn>( fn: (...args: TArgs) => TReturn ): Curry<TArgs, TReturn>; const add = curry((a: number, b: number, c: number) => a + b + c); const add5 = add(5); // Curry<[number, number], number> const add5and3 = add5(3); // Curry<[number], number> const result = add5and3(2); // number = 10 ``` --- ## Higher-Kinded Types ### Emulate HKT with Interface Lookup TypeScript doesn't natively support higher-kinded types, but they can be emulated with a registry pattern. ```typescript // Define a type-level registry for type constructors interface HKTRegistry { // Registered types go here via module augmentation } type HKT = keyof HKTRegistry; type Apply<F extends HKT, A> = HKTRegistry[F] extends { type: unknown } ? (HKTRegistry[F] & { arg: A })['type'] : never; // Register Array as a type constructor declare module './hkt' { interface HKTRegistry { Array: { type: Array<this['arg']> }; } } // Functor interface using HKT interface Functor<F extends HKT> { map<A, B>(fa: Apply<F, A>, f: (a: A) => B): Apply<F, B>; } ``` --- ## Builder Pattern ### Track Builder State in the Type System ```typescript type BuilderState = { hasName: boolean; hasAge: boolean; }; type Builder<State extends BuilderState, T = {}> = { setName(name: string): Builder<State & { hasName: true }, T & { name: string }>; setAge(age: number): Builder<State & { hasAge: true }, T & { age: number }>; } & (State['hasName'] extends true ? State['hasAge'] extends true ? { build(): T } : {} : {}); declare function createBuilder(): Builder<{ hasName: false; hasAge: false }>; const builder = createBuilder(); const user = builder.setName('Alice').setAge(30).build(); // user: { name: string } & { age: number } // Compile errors: // builder.build() - Error: build() not available until required fields set // builder.setName('Alice').build() - Error: age not set ``` ### Use Fluent Interface with Immutable Type Accumulation ```typescript class QueryBuilder<T extends Record<string, unknown> = Record<string, never>> { private conditions: string[] = []; private selectedFields: string[] = []; select<K extends string>(field: K): QueryBuilder<T & Record<K, unknown>> { this.selectedFields.push(field); return this as unknown as QueryBuilder<T & Record<K, unknown>>; } where(condition: string): this { this.conditions.push(condition); return this; } build(): { fields: string[]; conditions: string[] } { return { fields: this.selectedFields, conditions: this.conditions }; } } const query = new QueryBuilder() .select('id') .select('name') .where('active = true') .build(); ``` --- ## Common Generic Patterns ### MaybePromise ```typescript type MaybePromise<T> = T | Promise<T>; async function normalize<T>(value: MaybePromise<T>): Promise<T> { return await value; } ``` ### DeepPartial ```typescript type DeepPartial<T> = T extends (infer U)[] ? DeepPartial<U>[] : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T; ``` ### PathOf and Get ```typescript // PathOf: all dot-notation paths into an object type PathOf<T> = T extends object ? { [K in keyof T]: K extends string ? T[K] extends object ? K | `${K}.${PathOf<T[K]>}` : K : never }[keyof T] : never; // Get: value at a path type Get<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? Get<T[K], Rest> : never : P extends keyof T ? T[P] : never; ``` ### Prettify (Flatten Intersection Types for Readability) ```typescript type Prettify<T> = { [K in keyof T]: T[K] } & {}; type A = { id: string } & { name: string } & { age: number }; type B = Prettify<A>; // { id: string; name: string; age: number } // B displays as a single object in IDE hover, much more readable ``` ### RequireAtLeastOne ```typescript type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Omit<T, Keys> & { [K in Keys]-?: Required<Pick<T, K>> & Partial<Omit<T, K>> }[Keys]; interface ContactOptions { email?: string; phone?: string; address?: string; } type Contact = RequireAtLeastOne<ContactOptions>; // Must provide at least one of email, phone, or address ``` ### RequireExactlyOne ```typescript type RequireExactlyOne<T, Keys extends keyof T = keyof T> = Omit<T, Keys> & { [K in Keys]: Required<Pick<T, K>> & { [O in Exclude<Keys, K>]?: never } }[Keys]; interface PaymentMethod { creditCard?: { number: string }; bankTransfer?: { account: string }; paypal?: { email: string }; } type Payment = RequireExactlyOne<PaymentMethod>; // Must provide exactly one payment method ``` -
ts7-native-compiler.md 10.4 KB
# TypeScript 7 Native Compiler Reference Adoption knowledge for `typescript@7` — the Go-native compiler (formerly `tsgo` / `@typescript/native-preview`), stable on npm as `typescript@7.0.2` since 2026-07-08. > **Point-in-time warning:** sections 3–6 describe the TS 7.0.x state as of > 2026-08-08. The JavaScript compiler API is slated to return in **7.1+**, and > ecosystem tools re-port on their own schedules — verify the current 7.x state > before applying any of the "lockout" or "fallback" guidance below. ## Table of Contents 1. [What TS 7 Native Is](#what-ts-7-native-is) 2. [No JavaScript Compiler API](#no-javascript-compiler-api) 3. [Dual-Install Bin Ambiguity](#dual-install-bin-ambiguity) 4. [Ecosystem Lockout Until 7.1](#ecosystem-lockout-until-71) 5. [The Adoption Case: Measured](#the-adoption-case-measured) 6. [Go/No-Go Checklist](#gono-go-checklist) --- ## What TS 7 Native Is TypeScript 7 is the Go-based rewrite of the compiler (project "Corsa", previewed as `tsgo`). The npm package `typescript@7` ships: - **A `bin/tsc` shim** that launches the platform-native Go binary (delivered via optional dependencies like `@typescript/typescript-win32-x64`) - **No `lib/typescript.js`** — the package is a CLI, not a library Type-checking semantics are intended to be identical to the JS compiler; the announcement claimed 7.7–11.9× faster typechecks (real-world repos measure in and above that range — see [the adoption case](#the-adoption-case-measured)). TS 6.0 is the JS-based bridge release: it exists to modernise config defaults (strict-by-default, `module: esnext`, legacy options removed) so a 6.0-clean tsconfig upgrades to 7 with little or no change. A repo whose tsconfig *already* matches the strict modern defaults can skip the bridge entirely and adopt 7 directly — the bridge is for config migration, not a required step. One known hard break at the config level: **TS 7 errors on `baseUrl`** (TS5102). Repos using `baseUrl`-relative `paths` must rewrite them tsconfig-relative (`./src/*`, `../pkg/*`) — if they already are, deleting the `baseUrl` line changes nothing. ## No JavaScript Compiler API The single biggest adoption consequence, and the one that silently breaks repo tooling: ```javascript // Under typescript@7 (7.0.x): const ts = require("typescript"); // Error: Cannot find module — MODULE_NOT_FOUND // There is no lib/typescript.js; the package is a bin shim over a Go binary. ``` Anything that imports TypeScript **programmatically** — `ts.createSourceFile`, `ts.createProgram`, custom lint scripts, codemods, doc generators, `typescript-eslint` — has no compiler to import. Options, in order of preference: 1. **Make the tool AST-free.** For lint-style checks, a dependency-free scrub-then-scan (strip strings/comments with regex, then scan the residue for the forbidden pattern) needs no compiler at all. A worked production example: a D1-access lint gate (`check-no-raw-d1.mjs`) written as a plain Node script precisely so the typecheck compiler could change out from under it without breaking the gate. If a script *can* be dependency-free, that is the most durable shape — don't "improve" it into `require('typescript')`. 2. **Use an alternative parser** that doesn't depend on the TS package: swc, oxc, babel with the TypeScript plugin, or `@typescript-eslint`'s standalone parser pinned to a TS 5/6 peer. 3. **Pin the API consumer to an alias.** Install `typescript5` (`npm:typescript@^5.9.3`) or the `@typescript/typescript6` alias alongside `typescript@7`, and point the API consumer at the alias. This works but drags in the [bin ambiguity](#dual-install-bin-ambiguity) below — treat it as a transition state, not an end state. Audit before adopting: `npm ls typescript` in every workspace. If `typescript` is a **leaf** (nothing depends on it), nothing consumes the API and the switch is safe. If typescript-eslint or similar appears above it, you're in the [lockout](#ecosystem-lockout-until-71) case. Note the boundary: bundlers and test runners that transpile via **esbuild or swc** (vite, vitest, wrangler, tsup) never invoke tsc or its API — they are unaffected. The API break only bites tools that *import* the `typescript` package. ## Dual-Install Bin Ambiguity > Point-in-time: this section applies while a `typescript5` fallback alias is > installed next to `typescript@7`. Once the alias is retired the ambiguity > disappears. Keeping a TS 5 fallback during a soak period means root `node_modules` holds **two packages that both ship a `tsc` bin** (`typescript@7` and the `typescript5` alias). npm's bin-link order is **not deterministic** — observed in production: after installing the alias, `npx tsc` in the repo root resolved to **5.9.3**, not 7. Consequences: - **Bare `tsc` / `npx tsc` must not be trusted** in any directory whose `node_modules` holds both packages. `npx tsc --version` tells you which one *won the link race*, not which one your scripts should run. - **Package scripts must invoke explicit bin paths** while the alias exists: ```jsonc // package.json — explicit compiler paths while typescript5 alias is installed { "scripts": { // Native TS 7 — the gate "typecheck": "node node_modules/typescript/bin/tsc --noEmit && node node_modules/typescript/bin/tsc --noEmit -p web/tsconfig.json", // Classic TS 5 fallback — one-command cross-check during the soak "typecheck:tsc5": "node node_modules/typescript5/lib/tsc.js --noEmit && node node_modules/typescript5/lib/tsc.js --noEmit -p web/tsconfig.json" }, "devDependencies": { "typescript": "^7.0.2", "typescript5": "npm:typescript@^5.9.3" } } ``` - Workspaces with a **single** typescript install (e.g. a `web/` sub-package that only has TS 7) have an unambiguous `tsc` — bare invocations there are fine and don't need rewriting. - Prefer the vendored alias over `npx -p typescript@5.9.3` for the fallback: an escape hatch that reaches for the network/npx cache at fallback time can fail to resolve exactly when you need it offline. - **Retire the alias after the soak** (a few weeks of normal work with no native-compiler-attributable discrepancies): remove `typescript5` and the `*:tsc5` scripts, and scripts may return to bare `tsc` since the collision is gone. Leave a guard comment on the explicit paths until then so nobody "simplifies" them back early. ## Ecosystem Lockout Until 7.1 > Point-in-time: this is the TS 7.0.x situation. The JS API returns in 7.1+; > each tool then ports on its own schedule. Check each tool's current status > before treating it as blocked. Toolchains that embed the TS programmatic API to typecheck **non-TS file formats** cannot run on 7.0.x at all: | Tool | Why it's pinned | |------|-----------------| | `vue-tsc` | Wraps the TS API to check `.vue` SFC template/script blocks | | `svelte-check` | Same pattern for `.svelte` files | | Astro (`astro check`) | TS API over `.astro` frontmatter/components | | MDX type-checking | TS API over embedded JSX in `.mdx` | | `typescript-eslint` type-aware rules | `ts.createProgram` under the hood | These stay on TS 6 (or a `typescript6` alias) until the API lands in 7.1+ *and* each tool ships a port. This makes compiler choice a **stack-selection input**: - **Single-language TS/TSX stacks** (React, plain Node/Workers, anything where tsc only ever sees `.ts`/`.tsx`): adopt TS 7 immediately — nothing in the check path needs the API. - **Embedded-language stacks** (Vue, Svelte, Astro, MDX-heavy docs): the typecheck gate is welded to the TS 6 API for now. Adopting TS 7 for *speed* means either waiting for 7.1+ ports or splitting the gate (native tsc for `.ts`, tool-pinned TS 6 for the embedded formats) — added complexity that usually isn't worth it before the ports exist. ## The Adoption Case: Measured > Point-in-time: numbers from one production adoption on 2026-07-09 > (`typescript@7.0.2`); your repo will differ — measure your own. A ~60k-line Cloudflare Workers + React SPA repo (two tsconfigs) measured with hyperfine (warmup 1, 5 runs): | Typecheck | tsc 5.9.3 | tsc 7.0.2 | Speedup | |-----------|-----------|-----------|---------| | Worker config | 4.02 s | 0.53 s | 7.6× | | Web SPA config | 10.05 s | 0.63 s | 16.0× | | Combined | 14.07 s | 1.16 s | **12.1×** | Total adoption cost: **one config line** (removing `baseUrl` from the web tsconfig — its `paths` were already tsconfig-relative). The root tsconfig, already on TS 6-style strict defaults, needed zero changes; the 6.0 bridge was skipped entirely. Second-order win: at ~0.6 s the previously-too-slow SPA typecheck moved *into* the pre-land check gate — web-side type drift now surfaces at check time instead of at build time. A 12× compiler isn't just the same gate faster; it makes previously-rationed checks cheap enough to run always. Supply-chain footnote: first-stable adoption (`7.0.2` was one day old) sits inside the usual 7-day new-release cooldown for build deps. The mitigations that made an early land acceptable there: Microsoft-published, no install lifecycle scripts on the package or its platform binaries, and tsc running `--noEmit` (it never writes shipped artifacts). Weigh the same factors — or just wait out the cooldown. ## Go/No-Go Checklist Run through this before switching a repo's typecheck to `typescript@7`: 1. **API audit** — `npm ls typescript` in every workspace. TypeScript must be a leaf, or every dependent must have a 7.0-era answer (AST-free rewrite, alternative parser, or pinned alias). Embedded-language tools (vue-tsc/svelte-check/Astro/MDX) → wait for 7.1+ ports. 2. **Config audit** — remove `baseUrl` (TS5102 hard error); confirm `paths` are tsconfig-relative and resolution is unchanged under the old compiler first. 3. **Benchmark before/after** — hyperfine with warmup on your actual configs. The speedup is the justification; record it. 4. **Zero behaviour diff** — run old and new compilers over the same tree and compare emitted diagnostics. Identical error sets = go. Any discrepancy is an upstream bug report, not a "close enough". 5. **Keep the fallback documented** — vendored `typescript5` alias + explicit-path scripts + a `check:tsc5` mirror for the soak period, with the retirement condition written down (N weeks, no native-attributable discrepancies → remove alias, return to bare `tsc`). 6. **Full gate green** — the entire check pipeline (typecheck + lints + tests + build) on the new compiler before landing. -
type-system.md 16.6 KB
# TypeScript Type System Reference ## Table of Contents 1. [Literal Types](#literal-types) 2. [Discriminated Unions](#discriminated-unions) 3. [Branded/Nominal Types](#brandednominal-types) 4. [Template Literal Types](#template-literal-types) 5. [Recursive Types](#recursive-types) 6. [satisfies Operator](#satisfies-operator) 7. [Type Assertions](#type-assertions) 8. [Declaration Merging](#declaration-merging) 9. [Type-Level Arithmetic](#type-level-arithmetic) 10. [Variance](#variance) --- ## Literal Types ### Understand String, Number, and Boolean Literals Literal types restrict a value to one specific value rather than the broader primitive type. ```typescript // String literal type Direction = 'north' | 'south' | 'east' | 'west'; type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; // Number literal type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6; type HttpSuccess = 200 | 201 | 204; // Boolean literal type Truthy = true; type Falsy = false; // Mixed literal union type Status = 'pending' | 'fulfilled' | 'rejected'; type Result = 0 | 1 | -1; function move(direction: Direction): void { console.log(`Moving ${direction}`); } move('north'); // OK move('up'); // Error: Argument of type '"up"' is not assignable to parameter of type 'Direction' ``` ### Use const Assertions to Preserve Literal Types Without `as const`, TypeScript widens literals to their primitive types. With it, literals are preserved. ```typescript // Without as const - types are widened const config = { endpoint: '/api', // string retries: 3, // number methods: ['GET', 'POST'], // string[] }; // With as const - all literals preserved const CONFIG = { endpoint: '/api', // '/api' retries: 3, // 3 methods: ['GET', 'POST'], // readonly ['GET', 'POST'] } as const; type Endpoint = typeof CONFIG.endpoint; // '/api' type Retry = typeof CONFIG.retries; // 3 type Methods = typeof CONFIG.methods[number]; // 'GET' | 'POST' // as const on arrays const ROLES = ['admin', 'user', 'moderator'] as const; type Role = typeof ROLES[number]; // 'admin' | 'user' | 'moderator' // as const on function arguments function configure<T extends object>(opts: T): Readonly<T> { return Object.freeze(opts); } const opts = configure({ debug: true, port: 3000 } as const); // opts.debug is true (not boolean), opts.port is 3000 (not number) ``` ### Derive Union Types from const Arrays ```typescript const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE'] as const; type HttpMethod = typeof HTTP_METHODS[number]; // Enum alternative using as const object const Color = { Red: 'red', Green: 'green', Blue: 'blue', } as const; type Color = typeof Color[keyof typeof Color]; // 'red' | 'green' | 'blue' ``` --- ## Discriminated Unions ### Build Discriminated Unions with a Shared Literal Property Every variant shares a common property (the discriminant) with a unique literal type. ```typescript type Shape = | { kind: 'circle'; radius: number } | { kind: 'rectangle'; width: number; height: number } | { kind: 'triangle'; base: number; height: number }; function area(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2; case 'rectangle': return shape.width * shape.height; case 'triangle': return 0.5 * shape.base * shape.height; } } ``` ### Implement Exhaustiveness Checking with never When all union variants are handled, the remaining type is `never`. Passing `never` to a function that expects `never` causes a type error when a new variant is added. ```typescript function assertNever(value: never, message?: string): never { throw new Error(message ?? `Unhandled discriminated union member: ${JSON.stringify(value)}`); } type NetworkState = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: string } | { status: 'error'; error: Error }; function handleState(state: NetworkState): string { switch (state.status) { case 'idle': return 'Waiting...'; case 'loading': return 'Loading...'; case 'success': return state.data; case 'error': return state.error.message; default: return assertNever(state); // Compile error if case is missing } } ``` ### Model Result Types as Discriminated Unions ```typescript type Ok<T> = { ok: true; value: T }; type Err<E> = { ok: false; error: E }; type Result<T, E = Error> = Ok<T> | Err<E>; function divide(a: number, b: number): Result<number, string> { if (b === 0) return { ok: false, error: 'Division by zero' }; return { ok: true, value: a / b }; } const result = divide(10, 2); if (result.ok) { console.log(result.value); // number } else { console.error(result.error); // string } ``` --- ## Branded/Nominal Types ### Create Branded Types to Prevent Type Confusion TypeScript uses structural typing: two types with the same shape are interchangeable. Branding adds a phantom property to make them nominally distinct. ```typescript type Brand<T, B extends string> = T & { readonly __brand: B }; type UserId = Brand<string, 'UserId'>; type OrderId = Brand<string, 'OrderId'>; type Email = Brand<string, 'Email'>; type Dollars = Brand<number, 'Dollars'>; type Cents = Brand<number, 'Cents'>; // Without branding these are all just 'string' - interchangeable and unsafe. // With branding they are distinct. function getUser(id: UserId): void { /* ... */ } function getOrder(id: OrderId): void { /* ... */ } declare const userId: UserId; declare const orderId: OrderId; getUser(userId); // OK getUser(orderId); // Error: Argument of type 'OrderId' is not assignable to parameter of type 'UserId' ``` ### Write Validation Functions That Return Branded Types ```typescript function brandUserId(raw: string): UserId { return raw as UserId; } function parseEmail(raw: string): Email { if (!/^[^@]+@[^@]+\.[^@]+$/.test(raw)) { throw new Error(`Invalid email: ${raw}`); } return raw as Email; } function parseDollars(amount: number): Dollars { if (amount < 0) throw new Error('Dollars cannot be negative'); return amount as Dollars; } // Use in domain logic - type system enforces correct usage function sendInvoice(to: Email, amount: Dollars): void { /* ... */ } ``` ### Use Opaque Types via Unique Symbol (Advanced) For stricter encapsulation, use unique symbols as the brand key. ```typescript declare const _brand: unique symbol; type Opaque<T, Tag> = T & { readonly [_brand]: Tag }; type PositiveInt = Opaque<number, 'PositiveInt'>; function toPositiveInt(n: number): PositiveInt { if (!Number.isInteger(n) || n <= 0) { throw new Error(`Expected positive integer, got ${n}`); } return n as PositiveInt; } ``` --- ## Template Literal Types ### Build Type-Safe String Patterns Template literal types compose string literals at the type level. ```typescript type EventName<T extends string> = `on${Capitalize<T>}`; type ClickEvent = EventName<'click'>; // 'onClick' type ChangeEvent = EventName<'change'>; // 'onChange' type CSSProperty = 'margin' | 'padding'; type CSSUnit = 'px' | 'em' | 'rem' | '%'; type CSSValue = `${number}${CSSUnit}`; // '10px', '1.5em', etc. // Route parameter extraction type RouteParam<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Param | RouteParam<Rest> : T extends `${string}:${infer Param}` ? Param : never; type Params = RouteParam<'/users/:userId/posts/:postId'>; // 'userId' | 'postId' ``` ### Use String Manipulation Types ```typescript type Uppercased = Uppercase<'hello world'>; // 'HELLO WORLD' type Lowercased = Lowercase<'HELLO WORLD'>; // 'hello world' type Capitalized = Capitalize<'hello'>; // 'Hello' type Uncapitalized = Uncapitalize<'Hello'>; // 'hello' // Build getter/setter types type Getter<T extends string> = `get${Capitalize<T>}`; type Setter<T extends string> = `set${Capitalize<T>}`; type FieldName = 'name' | 'age' | 'email'; type Getters = { [K in FieldName as Getter<K>]: string }; // { getName: string; getAge: string; getEmail: string } // Build event handler types from object keys type EventHandlers<T> = { [K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void; }; interface FormFields { name: string; age: number; } type FormHandlers = EventHandlers<FormFields>; // { onNameChange: (value: string) => void; onAgeChange: (value: number) => void } ``` --- ## Recursive Types ### Define the JSON Type ```typescript type JsonPrimitive = string | number | boolean | null; type JsonArray = JsonValue[]; type JsonObject = { [key: string]: JsonValue }; type JsonValue = JsonPrimitive | JsonArray | JsonObject; // Usage const data: JsonValue = { name: 'Alice', scores: [1, 2, 3], address: { city: 'NYC', zip: null }, }; ``` ### Implement Deep Readonly and Deep Partial ```typescript type DeepReadonly<T> = T extends (infer U)[] ? ReadonlyArray<DeepReadonly<U>> : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T; type DeepPartial<T> = T extends (infer U)[] ? DeepPartial<U>[] : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T; interface Config { server: { host: string; port: number }; database: { url: string; poolSize: number }; } type ReadonlyConfig = DeepReadonly<Config>; // server.host and all nested props are readonly type PartialConfig = DeepPartial<Config>; // All nested props optional ``` ### Build Path Types for Safe Object Access ```typescript type PathOf<T, Sep extends string = '.'> = T extends object ? { [K in keyof T]: K extends string ? T[K] extends object ? K | `${K}${Sep}${PathOf<T[K], Sep>}` : K : never; }[keyof T] : never; type ValueAt<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? ValueAt<T[K], Rest> : never : P extends keyof T ? T[P] : never; interface User { id: string; profile: { name: string; address: { city: string } }; } type UserPath = PathOf<User>; // 'id' | 'profile' | 'profile.name' | 'profile.address' | 'profile.address.city' type CityType = ValueAt<User, 'profile.address.city'>; // string ``` --- ## satisfies Operator ### Validate Type Without Widening The `satisfies` operator checks that a value matches a type while preserving the most specific type. ```typescript // Problem without satisfies: type Palette = Record<string, [number, number, number] | string>; const palette1: Palette = { red: [255, 0, 0], green: '#00ff00', }; // palette1.red is [number, number, number] | string - information lost // With satisfies: const palette2 = { red: [255, 0, 0], green: '#00ff00', } satisfies Palette; // palette2.red is [number, number, number] - specific type preserved // palette2.green is string - specific type preserved palette2.red.map(v => v * 2); // OK - TypeScript knows it's an array palette2.green.toUpperCase(); // OK - TypeScript knows it's a string ``` ### Combine satisfies with as const ```typescript const routes = { home: '/', about: '/about', user: '/users/:id', } as const satisfies Record<string, `/${string}`>; // Routes values are literal types, not string type HomeRoute = typeof routes.home; // '/' ``` ### Use satisfies for Configuration Objects ```typescript interface PluginConfig { name: string; version: string; hooks?: { beforeBuild?: () => void; afterBuild?: () => void; }; } const myPlugin = { name: 'my-plugin', version: '1.0.0', hooks: { beforeBuild: () => console.log('building...'), }, } satisfies PluginConfig; // myPlugin.name is 'my-plugin' not string // TypeScript checks shape against PluginConfig at definition site ``` --- ## Type Assertions ### Understand When Assertions Are Safe Type assertions (`as T`) override TypeScript's type inference. They are safe only when you have external information the compiler cannot verify. ```typescript // SAFE: narrowing after a runtime check function processInput(input: unknown): string { if (typeof input === 'string') { return input; // narrowed, no assertion needed } // We know from domain logic this is always serializable return String(input); } // SAFE: DOM API returns Element | null, but we know the element exists const canvas = document.getElementById('canvas') as HTMLCanvasElement; // UNSAFE: asserting unrelated types const num = 42 as unknown as string; // compiles, crashes at runtime ``` ### Use Double Assertion as Escape Hatch When TypeScript refuses an assertion because types don't overlap, cast through `unknown`. ```typescript // Only do this when you have proof the cast is correct function forceType<T>(value: unknown): T { return value as T; } // Explicit escape: cast through unknown const risky = someValue as unknown as TargetType; ``` ### Prefer Type Guards Over Assertions ```typescript // BAD: assertion with no runtime check function getUser(data: unknown): User { return data as User; // unsafe, no verification } // GOOD: type guard with runtime verification function isUser(data: unknown): data is User { return ( typeof data === 'object' && data !== null && 'id' in data && typeof (data as Record<string, unknown>).id === 'string' && 'name' in data && typeof (data as Record<string, unknown>).name === 'string' ); } function getUser(data: unknown): User { if (!isUser(data)) throw new Error('Invalid user data'); return data; // safe, narrowed by type guard } ``` --- ## Declaration Merging ### Merge Interfaces to Extend Third-Party Types ```typescript // Original interface from a library interface Request { method: string; url: string; } // Your augmentation - merges with above interface Request { user?: { id: string; role: string }; requestId: string; } // Result: Request has method, url, user, requestId ``` ### Augment Modules to Add Types to External Packages ```typescript // express-augment.d.ts import 'express'; declare module 'express' { interface Request { user?: { id: string; role: 'admin' | 'user' }; sessionId: string; } } ``` ### Augment Global Scope ```typescript // global.d.ts declare global { interface Window { analytics: { track(event: string, properties?: Record<string, unknown>): void; }; } interface Array<T> { // Add a custom method to all arrays groupBy<K extends string>(keyFn: (item: T) => K): Record<K, T[]>; } } export {}; // Required to make this a module (not a script) ``` --- ## Type-Level Arithmetic ### Measure Tuple Lengths ```typescript type Length<T extends readonly unknown[]> = T['length']; type Three = Length<[1, 2, 3]>; // 3 type Zero = Length<[]>; // 0 ``` ### Build a Recursive Counter ```typescript // Build a tuple of length N, then read its length type BuildTuple<N extends number, T extends unknown[] = []> = T['length'] extends N ? T : BuildTuple<N, [...T, unknown]>; type Add<A extends number, B extends number> = Length<[...BuildTuple<A>, ...BuildTuple<B>]>; type Sum = Add<3, 4>; // 7 type Subtract<A extends number, B extends number> = BuildTuple<A> extends [...BuildTuple<B>, ...infer Rest] ? Length<Rest> : never; type Diff = Subtract<7, 3>; // 4 ``` --- ## Variance ### Understand Covariance and Contravariance - **Covariant**: A `Producer<Dog>` is assignable to `Producer<Animal>` (output position) - **Contravariant**: A `Consumer<Animal>` is assignable to `Consumer<Dog>` (input position) - **Invariant**: Neither assignment is safe ```typescript // Covariant: return type position type Producer<out T> = () => T; declare let animalProducer: Producer<Animal>; declare let dogProducer: Producer<Dog>; animalProducer = dogProducer; // OK - Dog is a subtype of Animal // Contravariant: parameter type position type Consumer<in T> = (value: T) => void; declare let animalConsumer: Consumer<Animal>; declare let dogConsumer: Consumer<Dog>; dogConsumer = animalConsumer; // OK - Consumer<Animal> handles any Animal including Dog animalConsumer = dogConsumer; // Error - Consumer<Dog> can't handle all Animals ``` ### Apply in/out Variance Annotations (TypeScript 4.7+) ```typescript interface Animal { name: string; } interface Dog extends Animal { breed: string; } // Explicitly mark variance for clarity and performance interface ReadableStream<out T> { // covariant - only produces T read(): T; } interface WritableStream<in T> { // contravariant - only consumes T write(value: T): void; } interface Transform<in TInput, out TOutput> { // bivariant transform(input: TInput): TOutput; } ``` ### Recognize Function Parameter Bivariance Trap ```typescript // strictFunctionTypes catches this type Callback = (event: MouseEvent) => void; type Handler = (event: Event) => void; // With strictFunctionTypes: NOT assignable (correct) // Without strictFunctionTypes: assignable (unsafe) ``` -
utility-types.md 12.9 KB
# TypeScript Utility Types Reference ## Table of Contents 1. [Built-in Utility Types](#built-in-utility-types) 2. [Custom Utility Types](#custom-utility-types) 3. [Type-Safe Object Operations](#type-safe-object-operations) 4. [Array/Tuple Utilities](#arraytuple-utilities) 5. [Function Utilities](#function-utilities) --- ## Built-in Utility Types ### Object Shape Utilities ```typescript // Partial<T> - Make all properties optional interface User { id: string; name: string; email: string; } type UpdateUser = Partial<User>; // { id?: string; name?: string; email?: string } function updateUser(id: string, patch: Partial<User>): User { /* ... */ } // Required<T> - Make all properties required interface Config { host?: string; port?: number; debug?: boolean; } type StrictConfig = Required<Config>; // { host: string; port: number; debug: boolean } // Readonly<T> - Make all properties readonly type ImmutableUser = Readonly<User>; // { readonly id: string; readonly name: string; ... } const frozen: ImmutableUser = { id: '1', name: 'Alice', email: 'a@b.com' }; // frozen.name = 'Bob'; // Error: cannot assign to 'name' because it is a read-only property // Record<K, T> - Create an object type with keys K and values T type UserMap = Record<string, User>; type StatusMap = Record<'active' | 'inactive' | 'banned', number>; type HttpStatus = Record<200 | 404 | 500, string>; // Pick<T, K> - Select a subset of properties type UserPreview = Pick<User, 'id' | 'name'>; // { id: string; name: string } type Credentials = Pick<User, 'email'>; // { email: string } // Omit<T, K> - Exclude specific properties type PublicUser = Omit<User, 'email'>; // { id: string; name: string } type NewUser = Omit<User, 'id'>; // { name: string; email: string } ``` ### Union Manipulation Utilities ```typescript // Exclude<T, U> - Remove U from union T type NonBoolean = Exclude<string | number | boolean, boolean>; // string | number type NonNullish = Exclude<string | null | undefined, null | undefined>; // string type NonString = Exclude<string | number | boolean, string>; // number | boolean // Extract<T, U> - Keep only members of T that are assignable to U type OnlyStrings = Extract<string | number | boolean, string>; // string type Primitives = Extract<string | number | { id: string }, string | number>; // string | number // NonNullable<T> - Remove null and undefined from T type SafeString = NonNullable<string | null | undefined>; // string type SafeUser = NonNullable<User | null | undefined>; // User ``` ### Function Utilities ```typescript function fetchData(url: string, timeout: number, headers: Record<string, string>): Promise<unknown> { return fetch(url); } // ReturnType<T> - Get the return type of a function type type FetchResult = ReturnType<typeof fetchData>; // Promise<unknown> type StringLength = ReturnType<typeof String.prototype.indexOf>; // number // Parameters<T> - Get parameters as a tuple type type FetchParams = Parameters<typeof fetchData>; // [url: string, timeout: number, headers: Record<string, string>] // Call a function with stored parameters function withDefaults<T extends (...args: unknown[]) => unknown>( fn: T, defaults: Partial<Parameters<T>> ) { /* ... */ } // ConstructorParameters<T> - Get constructor parameter types class HttpClient { constructor(baseUrl: string, timeout: number) {} } type HttpArgs = ConstructorParameters<typeof HttpClient>; // [string, number] // InstanceType<T> - Get the type of a class instance type ClientInstance = InstanceType<typeof HttpClient>; // HttpClient // ThisParameterType<T> - Extract the type of 'this' function greet(this: { name: string }, greeting: string): string { return `${greeting}, ${this.name}`; } type GreetThis = ThisParameterType<typeof greet>; // { name: string } // OmitThisParameter<T> - Remove this parameter from function type type GreetFn = OmitThisParameter<typeof greet>; // (greeting: string) => string ``` ### Awaited and String Utilities ```typescript // Awaited<T> - Recursively unwrap Promise type A = Awaited<Promise<string>>; // string type B = Awaited<Promise<Promise<number>>>; // number type C = Awaited<string | Promise<number>>; // string | number async function loadData(): Promise<User[]> { return []; } type LoadResult = Awaited<ReturnType<typeof loadData>>; // User[] // String manipulation (compile-time only, no runtime effect) type UpperName = Uppercase<'hello'>; // 'HELLO' type LowerName = Lowercase<'WORLD'>; // 'world' type CapName = Capitalize<'alice'>; // 'Alice' type UnCapName = Uncapitalize<'Hello'>; // 'hello' // Useful for generating method names type Methods<T extends string> = `get${Capitalize<T>}` | `set${Capitalize<T>}`; type NameMethods = Methods<'name' | 'age'>; // 'getName' | 'getAge' | 'setName' | 'setAge' ``` --- ## Custom Utility Types ### DeepReadonly ```typescript type DeepReadonly<T> = T extends (infer U)[] ? ReadonlyArray<DeepReadonly<U>> : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T; interface AppState { user: { id: string; profile: { name: string; bio: string } }; settings: { theme: string; notifications: boolean[] }; } type FrozenState = DeepReadonly<AppState>; declare const state: FrozenState; // state.user.profile.name = 'x'; // Error - deeply readonly ``` ### DeepPartial ```typescript type DeepPartial<T> = T extends (infer U)[] ? DeepPartial<U>[] : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T; // Useful for deep merge / patch operations function deepMerge<T>(target: T, patch: DeepPartial<T>): T { if (typeof patch !== 'object' || patch === null) return patch as T; const result = { ...target }; for (const key of Object.keys(patch) as (keyof T)[]) { const val = patch[key as keyof typeof patch]; if (val !== undefined) { (result[key] as unknown) = typeof val === 'object' && val !== null ? deepMerge(result[key] as object, val as DeepPartial<object>) : val; } } return result; } ``` ### Nullable and Optional ```typescript type Nullable<T> = T | null; type Optional<T> = T | undefined; type NullableOptional<T> = T | null | undefined; // Require at least one of specified keys type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Omit<T, Keys> & { [K in Keys]-?: Required<Pick<T, K>> & Partial<Omit<T, K>> }[Keys]; // Require exactly one of specified keys type RequireExactlyOne<T, Keys extends keyof T = keyof T> = Omit<T, Keys> & { [K in Keys]: Required<Pick<T, K>> & { [O in Exclude<Keys, K>]?: never } }[Keys]; ``` ### Merge and UnionToIntersection ```typescript // Merge two types, second overrides first type Merge<T, U> = Omit<T, keyof U> & U; type A = { id: string; name: string; active: boolean }; type B = { name: number; extra: string }; // name changes type type C = Merge<A, B>; // { id: string; active: boolean; name: number; extra: string } // Convert a union to an intersection type UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never; type IntersectedABC = UnionToIntersection<{ a: string } | { b: number } | { c: boolean }>; // { a: string } & { b: number } & { c: boolean } // Prettify - flatten intersection types for readable IDE output type Prettify<T> = { [K in keyof T]: T[K] } & {}; ``` ### Exact and StrictOmit ```typescript // Ensure no extra properties (useful in function params) type Exact<T, Shape> = T extends Shape ? Exclude<keyof T, keyof Shape> extends never ? T : never : never; // StrictOmit: errors if K is not in T (unlike Omit which silently ignores) type StrictOmit<T, K extends keyof T> = Omit<T, K>; ``` --- ## Type-Safe Object Operations ### Type-Safe pick ```typescript function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { return keys.reduce((acc, key) => { acc[key] = obj[key]; return acc; }, {} as Pick<T, K>); } const user: User = { id: '1', name: 'Alice', email: 'a@b.com' }; const preview = pick(user, ['id', 'name']); // { id: string; name: string } // TypeScript knows preview has only 'id' and 'name' ``` ### Type-Safe omit ```typescript function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> { const result = { ...obj }; keys.forEach((key) => delete result[key]); return result as Omit<T, K>; } const publicUser = omit(user, ['email']); // { id: string; name: string } ``` ### Type-Safe merge ```typescript function merge<T extends object, U extends object>(base: T, override: U): Merge<T, U> { return { ...base, ...override } as Merge<T, U>; } type Merge<T, U> = Omit<T, keyof U> & U; ``` ### Type-Safe diff (keys present in T but not U) ```typescript type Diff<T, U> = Pick<T, Exclude<keyof T, keyof U>>; type OnlyInA = Diff<{ a: string; b: number; c: boolean }, { b: number; d: string }>; // { a: string; c: boolean } ``` --- ## Array/Tuple Utilities ### Head, Tail, Last, Reverse ```typescript // First element of a tuple type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never; // All but first element type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : never; // Last element of a tuple type Last<T extends unknown[]> = T extends [...unknown[], infer L] ? L : never; // Reverse a tuple type Reverse<T extends unknown[], Acc extends unknown[] = []> = T extends [infer Head, ...infer Rest] ? Reverse<Rest, [Head, ...Acc]> : Acc; type H = Head<[string, number, boolean]>; // string type T = Tail<[string, number, boolean]>; // [number, boolean] type L = Last<[string, number, boolean]>; // boolean type R = Reverse<[1, 2, 3]>; // [3, 2, 1] ``` ### Flatten Types ```typescript // Flatten one level type Flatten<T extends unknown[]> = T extends (infer U)[] ? U : T; type F = Flatten<string[][]>; // string[] // Flatten nested arrays recursively type DeepFlatten<T> = T extends (infer U)[] ? U extends unknown[] ? DeepFlatten<U> : U : T; type Deep = DeepFlatten<string[][][]>; // string ``` ### Zip Two Tuples ```typescript type Zip<T extends unknown[], U extends unknown[]> = T extends [infer TH, ...infer TR] ? U extends [infer UH, ...infer UR] ? [[TH, UH], ...Zip<TR, UR>] : [] : []; type Zipped = Zip<[1, 2, 3], ['a', 'b', 'c']>; // [[1, 'a'], [2, 'b'], [3, 'c']] ``` ### Length and Indices ```typescript type Length<T extends readonly unknown[]> = T['length']; // Generate numeric union of indices type Indices<T extends readonly unknown[]> = Exclude<keyof T, keyof []>; type Len = Length<[1, 2, 3]>; // 3 type Idx = Indices<['a', 'b', 'c']>; // '0' | '1' | '2' ``` --- ## Function Utilities ### Promisify Type ```typescript // Convert a callback-style function type to one returning a Promise type Promisify<T extends (...args: unknown[]) => unknown> = T extends (...args: infer A) => infer R ? R extends Promise<unknown> ? T : (...args: A) => Promise<Awaited<R>> : never; type SyncFn = (x: number) => string; type AsyncFn = Promisify<SyncFn>; // (x: number) => Promise<string> ``` ### Curry Type ```typescript type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never; type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : never; type Curried<TArgs extends unknown[], TReturn> = TArgs extends [] ? TReturn : (arg: Head<TArgs>) => Curried<Tail<TArgs>, TReturn>; declare function curry<TArgs extends unknown[], TReturn>( fn: (...args: TArgs) => TReturn ): Curried<TArgs, TReturn>; const add = curry((a: number, b: number) => a + b); const inc = add(1); // Curried<[number], number> = (b: number) => number const two = inc(1); // number ``` ### Overload Helper ```typescript // Extract all overload signatures as a union type Overloads<T extends (...args: unknown[]) => unknown> = T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; (...args: infer A4): infer R4; } ? ((...args: A1) => R1) | ((...args: A2) => R2) | ((...args: A3) => R3) | ((...args: A4) => R4) : T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; } ? ((...args: A1) => R1) | ((...args: A2) => R2) | ((...args: A3) => R3) : T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2 } ? ((...args: A1) => R1) | ((...args: A2) => R2) : T; ``` ### Memoize with Type Safety ```typescript type AnyFn = (...args: unknown[]) => unknown; function memoize<T extends AnyFn>(fn: T): T { const cache = new Map<string, ReturnType<T>>(); return ((...args: Parameters<T>): ReturnType<T> => { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key) as ReturnType<T>; const result = fn(...args) as ReturnType<T>; cache.set(key, result); return result; }) as T; } const expensiveCalc = memoize((a: number, b: number): number => a * b); expensiveCalc(2, 3); // 6 - computed expensiveCalc(2, 3); // 6 - cached ```
-
-
scripts
-
check-typescript-facts.py 11 KB
#!/usr/bin/env python3 """Staleness verifier for typescript-ops: the version-bearing facts the skill encodes must stay real and cited. typescript-ops anchors its currency to a few version-bearing facts — the TypeScript major the prose assumes (feature floors like "TS 4.4+", "TypeScript 4.7+"), and the runtime-validation stack (zod, valibot). That is exactly the fact that drifts silently (SKILL-RESOURCE-PROTOCOL.md §7): a package leaves its documented major upstream, or the prose stops naming a package the catalog still commits to, and nobody notices for months. Two modes guard it: --offline (default, safe for PR CI): structural consistency, no network. * assets/typescript-facts.json parses; every entry has name + documented_major * every catalogued package is still named somewhere in the skill prose (SKILL.md / references/*.md) — the catalog can't drift from the docs * SKILL.md still carries a dated "as of 20XX" currency note --live (scheduled freshness.yml, never a PR gate): does each package's latest published major on npm still match the documented major? A newer major = the skill is behind reality (drift). Exit 7 if npm is unreachable. Usage: check-typescript-facts.py [--offline | --live] [--catalog FILE] [--skill DIR] [--json] [--timeout S] Input: argv flags only (no stdin). Output: stdout = findings (plain rows, or a --json envelope). Data only. Stderr: the verdict line, notices, errors. Exit: 0 ok, 2 usage, 3 catalog/skill missing, 4 catalog unparseable, 7 npm unreachable (live, advisory — never a real failure), 10 drift found (offline: uncited/undocumented/no currency note; live: published major newer than documented major) Examples: check-typescript-facts.py --offline # PR CI: catalog ⇆ prose consistency check-typescript-facts.py --live # weekly: every package's major still matches npm check-typescript-facts.py --offline --json | jq '.data[]' """ from __future__ import annotations import argparse import json import os import re import sys import urllib.error import urllib.parse import urllib.request from pathlib import Path EX_OK = 0 EX_USAGE = 2 EX_NOTFOUND = 3 EX_UNPARSEABLE = 4 EX_UNAVAILABLE = 7 EX_DRIFT = 10 HERE = Path(__file__).resolve().parent DEFAULT_CATALOG = HERE.parent / "assets" / "typescript-facts.json" DEFAULT_SKILL = HERE.parent DEFAULT_REGISTRY = "https://registry.npmjs.org" SCHEMA = "claude-mods.typescript-ops.facts/v1" CURRENCY_RE = re.compile(r"as of 20\d\d") def eprint(*a) -> None: print(*a, file=sys.stderr) class Term: """Minimal ANSI helper (term.sh is bash-only; per TERMINAL-DESIGN.md §9 the Python port is inline). Honors FORCE_COLOR / NO_COLOR / TERM_ASCII and the bound stream's TTY + encoding so piped data stays plain ASCII.""" _C = {"green": "\033[32m", "red": "\033[31m", "dim": "\033[2m", "off": "\033[0m"} def __init__(self, stream=sys.stderr) -> None: enc = (getattr(stream, "encoding", "") or "").lower() self.ascii = os.environ.get("TERM_ASCII") == "1" or "utf" not in enc if os.environ.get("FORCE_COLOR"): self.color = True elif (os.environ.get("NO_COLOR") is not None or os.environ.get("TERM") == "dumb" or not getattr(stream, "isatty", lambda: False)()): self.color = False else: self.color = True def c(self, name: str, text: str) -> str: return f"{self._C.get(name, '')}{text}{self._C['off']}" if self.color else text def mark(self, ok: bool) -> str: g = ("+" if self.ascii else "✓") if ok else ("x" if self.ascii else "✗") return self.c("green" if ok else "red", g) def load_catalog(path: Path) -> tuple[list[dict], str]: """Returns (packages, registry). Each package has name + documented_major.""" if not path.is_file(): eprint(f"error: package catalog not found: {path}") raise SystemExit(EX_NOTFOUND) try: data = json.loads(path.read_text(encoding="utf-8")) pkgs = data["packages"] if not isinstance(pkgs, list) or not pkgs: raise ValueError("'packages' must be a non-empty array") for p in pkgs: if not isinstance(p, dict) or "name" not in p or "documented_major" not in p: raise ValueError(f"package entry missing name/documented_major: {p!r}") dm = p["documented_major"] if not isinstance(dm, int) or isinstance(dm, bool) or dm < 0: raise ValueError(f"documented_major must be a non-negative int: {p!r}") registry = data.get("registry") or DEFAULT_REGISTRY return pkgs, registry except (json.JSONDecodeError, KeyError, TypeError, ValueError) as exc: eprint(f"error: could not parse catalog {path}: {exc}") raise SystemExit(EX_UNPARSEABLE) def read_corpus(skill_dir: Path) -> tuple[str, str]: """Returns (skill_md_text, all_prose_text) across SKILL.md + references/*.md.""" doc = skill_dir / "SKILL.md" if not doc.is_file(): eprint(f"error: SKILL.md not found under {skill_dir}") raise SystemExit(EX_NOTFOUND) skill_md = doc.read_text(encoding="utf-8") parts = [skill_md] for ref in sorted((skill_dir / "references").glob("*.md")): parts.append(ref.read_text(encoding="utf-8")) return skill_md, "\n".join(parts) def check_offline(pkgs: list[dict], skill_dir: Path) -> list[dict]: skill_md, corpus = read_corpus(skill_dir) findings: list[dict] = [] for p in pkgs: name = p["name"] # case-sensitive exact substring: npm names are case-sensitive (zod != Zod) if name not in corpus: findings.append({"package": name, "issue": "catalogued but not named in skill prose"}) if not CURRENCY_RE.search(skill_md): findings.append({"package": "(SKILL.md)", "issue": "no dated 'as of 20XX' currency note"}) return findings def npm_latest(registry: str, name: str, timeout: float) -> tuple[str, object]: """Return ('ok', version_str) | ('gone', None) | ('unreachable', info).""" url = registry.rstrip("/") + "/" + urllib.parse.quote(name, safe="") + "/latest" req = urllib.request.Request(url, method="GET", headers={"User-Agent": "claude-mods-typescript-ops-check/1", "Accept": "application/json"}) try: with urllib.request.urlopen(req, timeout=timeout) as resp: data = json.loads(resp.read().decode("utf-8")) return ("ok", data.get("version", "")) except urllib.error.HTTPError as exc: if exc.code in (404, 410): return ("gone", None) return ("unreachable", exc.code) # 5xx etc: transient, not a content finding except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc: return ("unreachable", str(getattr(exc, "reason", exc))) def major_of(version: str) -> int | None: m = re.match(r"\D*(\d+)", version or "") return int(m.group(1)) if m else None def check_live(pkgs: list[dict], registry: str, timeout: float) -> tuple[list[dict], list[dict]]: drift: list[dict] = [] unreachable: list[dict] = [] for p in pkgs: name = p["name"] doc = p["documented_major"] status, info = npm_latest(registry, name, timeout) if status == "gone": drift.append({"package": name, "issue": "no longer resolves on npm (404)"}) elif status != "ok": unreachable.append({"package": name, "issue": f"unreachable: {info}"}) else: live_major = major_of(str(info)) if live_major is None: unreachable.append({"package": name, "issue": f"could not parse version {info!r}"}) elif live_major > doc: drift.append({"package": name, "issue": f"npm@{info} major {live_major} > documented major {doc}"}) return drift, unreachable def main(argv: list[str]) -> int: p = argparse.ArgumentParser( prog="check-typescript-facts.py", description="Verify typescript-ops' version-bearing facts stay cited (offline) and current on npm (live).", ) mode = p.add_mutually_exclusive_group() mode.add_argument("--offline", action="store_true", help="structural consistency, no network (default)") mode.add_argument("--live", action="store_true", help="check every package's latest major still matches npm") p.add_argument("--catalog", default=str(DEFAULT_CATALOG), help="facts catalog JSON") p.add_argument("--skill", default=str(DEFAULT_SKILL), help="skill directory (SKILL.md + references/)") p.add_argument("--timeout", type=float, default=10.0, help="per-request timeout seconds (live)") p.add_argument("--json", action="store_true", help="emit a JSON envelope") try: args = p.parse_args(argv) except SystemExit as exc: return EX_USAGE if exc.code not in (0, None) else (exc.code or EX_OK) pkgs, registry = load_catalog(Path(args.catalog)) live = args.live and not args.offline t = Term(sys.stderr) if live: drift, unreachable = check_live(pkgs, registry, args.timeout) findings = drift + unreachable if args.json: print(json.dumps({ "data": findings, "meta": {"mode": "live", "packages_checked": len(pkgs), "drift": len(drift), "unreachable": len(unreachable), "registry": registry, "schema": SCHEMA}, }, indent=2)) else: for f in drift: print(f"DRIFT {f['package']}: {f['issue']}") for f in unreachable: print(f"UNREACH {f['package']}: {f['issue']}") # §7: confirmed drift -> 10; else transient/unreachable -> 7 (advisory); else 0. if drift: eprint(f"{t.mark(False)} ts-facts/live: {len(drift)} package(s) drifted from documented major " f"{t.c('dim', '(' + registry + ')')}") return EX_DRIFT if unreachable: eprint(f"{t.mark(False)} ts-facts/live: npm unreachable for " f"{len(unreachable)}/{len(pkgs)} {t.c('dim', '(advisory - retry next run)')}") return EX_UNAVAILABLE eprint(f"{t.mark(True)} ts-facts/live: all {len(pkgs)} package(s) match documented major on npm") return EX_OK # offline (default) findings = check_offline(pkgs, Path(args.skill)) if args.json: print(json.dumps({ "data": findings, "meta": {"mode": "offline", "packages_checked": len(pkgs), "drift": len(findings), "consistent": not findings, "schema": SCHEMA}, }, indent=2)) else: for f in findings: print(f"DRIFT {f['package']}: {f['issue']}") ok = not findings eprint(f"{t.mark(ok)} ts-facts/offline: {len(pkgs)} package(s) checked, " f"{len(findings)} inconsistency {t.c('dim', '(catalog vs skill prose)')}") return EX_DRIFT if findings else EX_OK if __name__ == "__main__": sys.exit(main(sys.argv[1:]))
-
-
tests
-
run.sh 5.2 KB
#!/usr/bin/env bash # Self-test for the typescript-ops skill. # # Offline-deterministic (no network, no TypeScript compiler required). Asserts # structural integrity (frontmatter, references present + linked) and — the # load-bearing check — the staleness verifier contract (SKILL-RESOURCE-PROTOCOL # §7, §10): the catalogued version-bearing facts (TypeScript major, zod, # valibot) stay named in the prose and the dated currency note stays present. # Resolves paths relative to itself so it works in the repo and once installed # to ~/.claude/skills/typescript-ops/. # # Usage: bash tests/run.sh # Exit: 0 all pass, 1 one or more failures set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL="$(dirname "$HERE")" DOC="$SKILL/SKILL.md" REF="$SKILL/references" PASS=0; FAIL=0 ok() { PASS=$((PASS+1)); printf ' PASS %s\n' "$1"; } no() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; } has() { case "$2" in *"$1"*) ok "$3";; *) no "$3 (missing '$1')";; esac; } echo "=== typescript-ops self-test ===" # ── SKILL.md frontmatter ─────────────────────────────────────────────────── echo "-- frontmatter --" [[ -f "$DOC" ]] && ok "SKILL.md present" || { no "SKILL.md missing"; echo "=== $PASS passed, $FAIL failed ==="; exit 1; } [[ "$(sed -n '1p' "$DOC")" == "---" ]] && ok "frontmatter fence opens at line 1" || no "no opening frontmatter fence" doc="$(cat "$DOC")" has 'name: typescript-ops' "$doc" "frontmatter declares name: typescript-ops" has 'description:' "$doc" "frontmatter has description" has 'license: MIT' "$doc" "frontmatter declares license" has 'author: claude-mods' "$doc" "frontmatter declares metadata.author" # ── references: the 6 documented files exist ─────────────────────────────── echo "-- references present --" EXPECT=(type-system utility-types generics-patterns config-strict ts7-native-compiler ecosystem) for r in "${EXPECT[@]}"; do f="$REF/$r.md" [[ -f "$f" ]] && ok "$r.md present" || no "$r.md missing" done # ── every references/ citation in SKILL.md resolves (no ghost refs) ──────── # typescript-ops cites its references as inline code spans (`./references/x.md`), # not markdown links — extract those spans and confirm each file exists. echo "-- cited references resolve --" linked=0; broken=0 while IFS= read -r rel; do [[ -z "$rel" ]] && continue linked=$((linked+1)) [[ -f "$SKILL/$rel" ]] || { no "SKILL.md cites missing file: $rel"; broken=$((broken+1)); } done < <(grep -oE '`(\./)?references/[^`#]+\.md`' "$DOC" | sed -E 's/^`//; s/`$//; s/^\.\///' | sort -u) [[ "$linked" -gt 0 ]] && ok "SKILL.md cites its references ($linked unique)" || no "SKILL.md cites no references" [[ "$broken" -eq 0 ]] && ok "all cited reference files resolve" || no "$broken reference citation(s) broken" # ── dated currency note present (verifier depends on it) ─────────────────── echo "-- currency note --" grep -qE 'as of 20[0-9]{2}' "$DOC" && ok "dated 'as of 20XX' currency note present" || no "no dated currency note" # ── staleness verifier: offline contract (SKILL-RESOURCE-PROTOCOL §7) ─────── echo "-- check-typescript-facts.py (offline) --" VERIFIER="$SKILL/scripts/check-typescript-facts.py" CATALOG="$SKILL/assets/typescript-facts.json" ec() { local want="$1" lbl="$2"; shift 2; "$@" >/dev/null 2>&1; local got=$? [[ "$got" == "$want" ]] && ok "$lbl (exit $got)" || no "$lbl (want $want got $got)"; } # Pick a python that actually executes — skips the Windows Store python3 stub. PY="" for c in python python3 py; do if command -v "$c" >/dev/null 2>&1 && "$c" -c "" >/dev/null 2>&1; then PY="$c"; break; fi done [[ -f "$VERIFIER" ]] && ok "verifier present" || no "verifier missing" [[ -f "$CATALOG" ]] && ok "facts catalog present" || no "catalog missing" has "scripts/check-typescript-facts.py" "$doc" "verifier cited from SKILL.md" if [[ -n "$PY" ]]; then TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT ec 0 "py_compile" "$PY" -m py_compile "$VERIFIER" ec 0 "--help" "$PY" "$VERIFIER" --help ec 0 "--offline consistent" "$PY" "$VERIFIER" --offline ec 2 "bad flag -> 2" "$PY" "$VERIFIER" --bogus ec 2 "conflicting modes -> 2" "$PY" "$VERIFIER" --offline --live jout="$("$PY" "$VERIFIER" --offline --json 2>/dev/null)" has 'claude-mods.typescript-ops.facts/v1' "$jout" "--json envelope schema" ec 3 "missing catalog -> 3" "$PY" "$VERIFIER" --offline --catalog "$TMP/nope.json" printf '{"packages":"x"}' > "$TMP/bad.json" ec 4 "malformed catalog -> 4" "$PY" "$VERIFIER" --offline --catalog "$TMP/bad.json" printf '{"packages":[{"name":"zzznotreal","documented_major":3}]}' > "$TMP/drift.json" ec 10 "uncited package -> 10" "$PY" "$VERIFIER" --offline --catalog "$TMP/drift.json" else no "no working python to exercise the verifier" fi # ── summary ──────────────────────────────────────────────────────────────── echo "=== $PASS passed, $FAIL failed ===" [[ "$FAIL" -eq 0 ]] || exit 1
-
-
SKILL.md 13 KB
--- name: typescript-ops description: "TypeScript type system, generics, utility types, strict mode, and ecosystem patterns. Use for: typescript, ts, type, generic, utility type, Partial, Pick, Omit, Record, Exclude, Extract, ReturnType, Parameters, keyof, typeof, infer, mapped type, conditional type, template literal type, discriminated union, type guard, type assertion, type narrowing, tsconfig, strict mode, declaration file, zod, valibot, typescript 7, tsgo, native compiler." license: MIT allowed-tools: "Read Write Bash" metadata: author: claude-mods related-skills: react-ops, testing-ops --- # TypeScript Operations Comprehensive TypeScript skill covering the type system, generics, and production patterns. > Ecosystem facts verified as of 2026-08-08 (TypeScript 7, Zod 4, Valibot 1). **Staleness check:** `python scripts/check-typescript-facts.py --offline` asserts the catalogued version-bearing facts (TypeScript major, zod, valibot) are still named in the prose and the dated currency note above is present; run `--live` to confirm each package's npm major still matches the documented major. Catalog: `assets/typescript-facts.json`. ## Type Narrowing Decision Tree ``` How to narrow a type? │ ├─ Primitive type check │ └─ typeof: typeof x === "string" │ ├─ Instance check │ └─ instanceof: x instanceof Date │ ├─ Property existence │ └─ in: "email" in user │ ├─ Discriminated union │ └─ switch on literal field: switch (event.type) │ ├─ Null/undefined check │ └─ Truthiness: if (x) or if (x != null) │ ├─ Custom logic │ └─ Type predicate: function isUser(x: unknown): x is User │ └─ Assertion (you know better than TS) └─ as: value as string (escape hatch, avoid when possible) ``` ### Type Guard Example ```typescript interface Dog { bark(): void; breed: string } interface Cat { meow(): void; color: string } function isDog(pet: Dog | Cat): pet is Dog { return "bark" in pet; } function handlePet(pet: Dog | Cat) { if (isDog(pet)) { pet.bark(); // TS knows it's Dog here } else { pet.meow(); // TS knows it's Cat here } } ``` ### Discriminated Unions ```typescript type Result<T> = | { status: "success"; data: T } | { status: "error"; error: string } | { status: "loading" }; function handle<T>(result: Result<T>) { switch (result.status) { case "success": return result.data; // data is available case "error": throw new Error(result.error); // error is available case "loading": return null; } // Exhaustiveness check: result is `never` here const _exhaustive: never = result; } ``` ## Utility Types Cheat Sheet | Utility | What It Does | Example | |---------|-------------|---------| | `Partial<T>` | All props optional | `Partial<User>` for update payloads | | `Required<T>` | All props required | `Required<Config>` for validated config | | `Readonly<T>` | All props readonly | `Readonly<State>` for immutable state | | `Pick<T, K>` | Select specific props | `Pick<User, "id" \| "name">` | | `Omit<T, K>` | Remove specific props | `Omit<User, "password">` | | `Record<K, V>` | Object with typed keys/values | `Record<string, number>` | | `Exclude<U, E>` | Remove types from union | `Exclude<Status, "deleted">` | | `Extract<U, E>` | Keep types from union | `Extract<Event, { type: "click" }>` | | `NonNullable<T>` | Remove null/undefined | `NonNullable<string \| null>` | | `ReturnType<F>` | Function return type | `ReturnType<typeof fetchUser>` | | `Parameters<F>` | Function params as tuple | `Parameters<typeof createUser>` | | `Awaited<T>` | Unwrap Promise type | `Awaited<Promise<User>>` = `User` | ## Generic Patterns ### Constrained Generics ```typescript // Basic constraint function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } // Multiple constraints function merge<T extends object, U extends object>(a: T, b: U): T & U { return { ...a, ...b }; } // Default generic type type ApiResponse<T = unknown> = { data: T; status: number; }; ``` ### Conditional Types ```typescript // Basic conditional type IsString<T> = T extends string ? true : false; // infer keyword - extract inner type type UnwrapPromise<T> = T extends Promise<infer U> ? U : T; type UnwrapArray<T> = T extends (infer U)[] ? U : T; // Distributive conditional (distributes over union) type ToArray<T> = T extends any ? T[] : never; // ToArray<string | number> = string[] | number[] // Prevent distribution with wrapping type ToArrayNonDist<T> = [T] extends [any] ? T[] : never; // ToArrayNonDist<string | number> = (string | number)[] ``` ### Mapped Types ```typescript // Make all properties optional and nullable type Nullable<T> = { [K in keyof T]: T[K] | null }; // Add prefix to keys type Prefixed<T, P extends string> = { [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K]; }; // Prefixed<{ name: string }, "get"> = { getName: string } // Filter keys by value type type StringKeys<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K]; }; ``` **Deep dive**: Load `./references/generics-patterns.md` for advanced type-level programming, recursive types, template literal types. ## Modern Language Features (TypeScript 5.x → 6.0) | Feature | Since | What It Gives You | |---------|-------|-------------------| | `satisfies` operator | 4.9 | Check a value against a type without widening it | | Standard (TC39) decorators | 5.0 | `@decorator` on classes/methods without `experimentalDecorators` | | `const` type parameters | 5.0 | `function f<const T>(x: T)` infers literal types without `as const` at call sites | | `using` declarations | 5.2 | Explicit resource management (`Symbol.dispose`), auto-cleanup at scope exit | | Inferred type predicates | 5.5 | `arr.filter(x => x !== null)` narrows without a hand-written `x is T` guard | | `verbatimModuleSyntax` | 5.0 | Enforces `import type` for type-only imports — replaces `importsNotUsedAsValues` | ```typescript // const type parameters (5.0) - literal inference without as const function routes<const T extends readonly string[]>(paths: T): T { return paths; } const r = routes(["/home", "/about"]); // readonly ["/home", "/about"], not string[] // using declarations (5.2) - deterministic cleanup function readConfig() { using file = openFile("config.json"); // file[Symbol.dispose]() runs at scope exit return parse(file.contents); } // Inferred type predicates (5.5) - no manual guard needed const names = ["a", null, "b"].filter(x => x !== null); // string[], not (string | null)[] ``` ### TypeScript 6.0 (The Bridge Release) TS 6.0 is the last release on the JavaScript-based compiler — it exists to bridge to the native (Go) compiler in TS 7, so its headline is stricter, modernised defaults: - **`strict: true` is the default** — a tsconfig that never set it now gets full strict checks - **Defaults modernised**: `module: esnext`, `target: es2025`; `es2025` lib ships types for Temporal, `Map.getOrInsert`, `RegExp.escape` - **Legacy options removed**: `moduleResolution: classic`; `module: amd/umd/system/none`; minimum `target` is now ES2015 (`es5` deprecated) - **Interop always on**: `esModuleInterop` / `allowSyntheticDefaultImports` can no longer be disabled - New `--stableTypeOrdering` flag eases 6.0 → 7.0 migration diffing ### TypeScript 7 (Current Major — Native Compiler) TS 7 is the Go-native rewrite (formerly `tsgo`), stable on npm since 2026-07-08. Same checking semantics, ~8–16× faster typechecks — but the package ships **only a `bin/tsc` shim, no JavaScript compiler API**: `require('typescript')` throws `MODULE_NOT_FOUND` on 7.0.x (the API returns in 7.1+). Consequences that gate adoption: - Repo tooling using the TS programmatic API (`ts.createSourceFile`, custom lint scripts, codemods, typescript-eslint) must go AST-free, switch parser, or pin an alias - Tools typechecking embedded languages (vue-tsc, svelte-check, Astro, MDX) are pinned to TS 6 until they port to the 7.1+ API — a real stack-selection input - A tsconfig already on TS 6 defaults adopts directly (the 6.0 bridge is skippable); `baseUrl` is a hard error (TS5102) - Running a `typescript5` fallback alias alongside 7 makes bare `npx tsc` ambiguous — scripts must use explicit compiler paths during the soak **Deep dive**: Load `./references/ts7-native-compiler.md` for the no-JS-API workarounds, dual-install bin ambiguity, ecosystem lockout table, measured adoption benchmarks (12.1×), and the go/no-go checklist. ## tsconfig Quick Reference ```jsonc { "compilerOptions": { // Strict mode (default in TS 6; state it explicitly anyway) "strict": true, // Enables all strict checks "noUncheckedIndexedAccess": true, // arr[0] is T | undefined // Module system (TS 6 defaults to module: esnext; interop is always on) "module": "esnext", // or "nodenext" for Node "moduleResolution": "bundler", // or "nodenext" // Output (TS 6 defaults target to es2025; min supported is es2015) "target": "es2022", "outDir": "dist", "declaration": true, // Generate .d.ts "sourceMap": true, // Paths — tsconfig-relative; don't add baseUrl (TS 7 hard-errors on it, TS5102) "paths": { "@/*": ["./src/*"] }, // Strictness extras "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "forceConsistentCasingInFileNames": true }, "include": ["src"], "exclude": ["node_modules", "dist"] } ``` **Deep dive**: Load `./references/config-strict.md` for strict mode migration, monorepo config, project references. ## Common Gotchas | Gotcha | Why | Fix | |--------|-----|-----| | `any` leaks | `any` disables type checking for everything it touches | Use `unknown` + narrowing instead | | `as` assertions hide bugs | Assertion doesn't check at runtime | Use type guards or validation (Zod) | | `enum` quirks | Numeric enums are not type-safe, reverse mappings confuse | Use `as const` objects or string literal unions | | `object` vs `Record` vs `{}` | `{}` matches any non-null value, `object` is non-primitive | Use `Record<string, unknown>` for "any object" | | Array index access | `arr[999]` returns `T` not `T \| undefined` by default | Enable `noUncheckedIndexedAccess` | | Optional vs undefined | `{ x?: string }` allows missing key, `{ x: string \| undefined }` requires key | Be explicit about which you mean | | `!` non-null assertion | Silences null checks, no runtime effect | Use `?? defaultValue` or proper null check | | Structural typing surprise | `{ a: 1, b: 2 }` assignable to `{ a: number }` | Use branded types for nominal typing | ## Branded / Nominal Types ```typescript // Prevent accidentally mixing types that are structurally identical type UserId = string & { readonly __brand: "UserId" }; type OrderId = string & { readonly __brand: "OrderId" }; function createUserId(id: string): UserId { return id as UserId; } function getUser(id: UserId) { /* ... */ } const userId = createUserId("u-123"); const orderId = "o-456" as OrderId; getUser(userId); // OK getUser(orderId); // Error: OrderId not assignable to UserId ``` ## Runtime Validation (Zod 4) ```typescript import { z } from "zod"; // Define schema (Zod 4: string formats are top-level - z.email(), not z.string().email()) const UserSchema = z.object({ id: z.number(), name: z.string().min(1), email: z.email(), role: z.enum(["admin", "user"]), settings: z.object({ theme: z.enum(["light", "dark"]).default("light"), }).optional(), }); // Infer type from schema type User = z.infer<typeof UserSchema>; // Validate const user = UserSchema.parse(untrustedData); // throws on invalid const result = UserSchema.safeParse(untrustedData); // returns { success, data/error } ``` **Zod 4 changes to know** (if you learned Zod 3): string formats moved to the top level (`z.email()`, `z.uuid()`, `z.url()` — the `z.string().email()` method form is deprecated); error customisation unified under a single `error` param (`invalid_type_error` / `required_error` dropped); much faster parsing and a tree-shakeable `zod/mini` entry point. ## Reference Files Load these for deep-dive topics. Each is self-contained. | Reference | When to Load | |-----------|-------------| | `./references/type-system.md` | Advanced types, branded types, type-level programming, satisfies operator | | `./references/generics-patterns.md` | Generic constraints, conditional types, mapped types, template literals, recursive types | | `./references/utility-types.md` | All built-in utility types with examples, custom utility types | | `./references/config-strict.md` | tsconfig deep dive, strict mode migration, project references, monorepo setup | | `./references/ts7-native-compiler.md` | Adopting the TS 7 native (Go) compiler: no JS API, bin ambiguity, ecosystem lockout, benchmarked go/no-go | | `./references/ecosystem.md` | Zod/Valibot, type-safe API clients, ORM types, testing with Vitest | ## See Also - `testing-ops` - Cross-language testing strategies - `ci-cd-ops` - TypeScript CI pipelines, type checking in CI
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.