TypeScript
This skill should be used when the user asks to "write TypeScript", "create a TS module", "implement a TypeScript class", "model this type", "add types", "type this function", "use generics", "use branded types", "use discriminated unions", "fix this TypeScript", "refactor this TS code", "use modern TypeScript", "configure tsconfig", or any task involving writing, reviewing, or improving TypeScript code. Provides best practices for type modeling, modern language features (TypeScript 5.5-5.8), error handling, module patterns, and project configuration. This skill covers TypeScript (.ts/.tsx) files, NOT plain JavaScript with JSDoc.
What this skill does
# TypeScript
Best practices for writing modern, type-safe TypeScript. Covers type modeling, modern compiler features (5.5-5.8), error handling, module patterns, and project configuration.
This skill targets `.ts` and `.tsx` files. For plain JavaScript with JSDoc type annotations, use the JavaScript skill instead.
## Project Setup
### tsconfig.json
Enable strict mode in `tsconfig.json` for every TypeScript project:
```json
{
"compilerOptions": {
"strict": true,
"target": "ES2024",
"module": "nodenext",
"moduleResolution": "nodenext",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noUncheckedIndexedAccess": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
```
Adjust `target`, `module`, and `moduleResolution` to match the project's runtime:
- **Node.js projects:** `"module": "nodenext"`, `"moduleResolution": "nodenext"`
- **Bundler-driven projects:** `"module": "preserve"`, `"moduleResolution": "bundler"`
- **Direct execution (Node.js 22.6+):** Add `"erasableSyntaxOnly": true` to ensure all TS-specific syntax can be stripped without altering runtime behavior. Avoid `enum`, `namespace`, and parameter properties (`constructor(public x: number)`) when this flag is enabled.
### Notable Compiler Options (5.5-5.8)
| Option | Version | Purpose |
|--------|---------|---------|
| `noUncheckedSideEffectImports` | 5.6 | Errors on unresolvable `import "polyfill"` |
| `erasableSyntaxOnly` | 5.8 | Restricts to type-erasable syntax for direct execution |
| `verbatimModuleSyntax` | 5.4 | Enforces explicit `import type` for type-only imports |
| `noUncheckedIndexedAccess` | 4.1 | Adds `undefined` to index signature results |
## Type Modeling
### Prefer Inference Over Annotation
Let TypeScript infer types when the result is unambiguous. Annotate return types on public API surfaces and when inference would be too wide:
```typescript
// Inferred — no annotation needed
const count = items.length;
const doubled = numbers.map(n => n * 2);
// Annotate: public API, inference would be too wide
function parseConfig(raw: string): AppConfig {
return JSON.parse(raw) as AppConfig;
}
```
### Discriminated Unions
Model variant types using a shared literal discriminant property. Use exhaustiveness checking with `never` to catch unhandled cases:
```typescript
type Result<T> =
| { kind: 'success'; value: T }
| { kind: 'error'; error: Error };
function handle<T>(result: Result<T>): T {
switch (result.kind) {
case 'success': return result.value;
case 'error': throw result.error;
default: {
const _exhaustive: never = result;
throw new Error(`Unhandled case: ${_exhaustive}`);
}
}
}
```
### The `satisfies` Operator
Use `satisfies` to validate a value conforms to a type while preserving narrower inference. Prefer `satisfies` over `as` for type validation:
```typescript
type Route = { path: string; auth: boolean };
type Routes = Record<string, Route>;
// Type annotation: loses key knowledge
const annotated: Routes = { home: { path: '/', auth: false } };
annotated.home; // Route — no key autocomplete
// satisfies: validates AND preserves literal keys
const checked = {
home: { path: '/', auth: false },
dashboard: { path: '/dash', auth: true },
} satisfies Routes;
checked.home; // { path: string; auth: boolean } — keys autocomplete
// as const satisfies: preserves literal values too
const frozen = {
home: { path: '/', auth: false },
} as const satisfies Routes;
frozen.home.path; // '/' (literal type)
```
### Branded Types
Create nominally distinct types from the same underlying type to prevent accidental interchange:
```typescript
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [__brand]: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function createUserId(id: string): UserId {
if (!id) throw new Error('Invalid user ID');
return id as UserId;
}
function lookupUser(id: UserId): User { /* ... */ }
lookupUser(createUserId('abc')); // OK
lookupUser('abc'); // Error: string is not UserId
```
### `NoInfer<T>`
Use `NoInfer` (TS 5.4+) to control which parameter TypeScript infers a generic from:
```typescript
function createFSM<TState extends string>(
states: TState[],
initial: NoInfer<TState> // Must be one of the states, inferred from `states`
) { /* ... */ }
createFSM(['idle', 'running', 'done'], 'idle'); // OK
createFSM(['idle', 'running', 'done'], 'invalid'); // Error
```
### Const Type Parameters
Use `const` type parameters (TS 5.0+) to infer literal types instead of widened types:
```typescript
function createRoutes<const T extends readonly Route[]>(routes: T) {
return routes;
}
// Infers tuple of literal types, not Route[]
```
For advanced patterns including template literal types, mapped types, conditional types, and complex generics, consult `references/type-patterns.md`.
## Modern Language Features
### Inferred Type Predicates (TS 5.5)
TypeScript 5.5 infers type predicates from function bodies. Manual type guards are no longer needed for simple narrowing:
```typescript
// TS 5.5+: automatic inference, no annotation needed
const validUsers = users.filter(user => user !== null);
// Inferred as User[] (not (User | null)[])
```
Inference requires: a single return statement, no parameter mutation, and a boolean expression tied to a parameter refinement. Truthiness checks (`!!x`) infer predicates for object types but not for numbers (where `0` is falsy).
### Explicit Resource Management (Stage 3)
Use `using` and `await using` for automatic resource cleanup:
```typescript
function readFile(path: string) {
using handle = openFile(path); // Symbol.dispose called at scope exit
return handle.read();
}
async function connectDB() {
await using conn = await pool.getConnection(); // Symbol.asyncDispose called
return conn.query('SELECT 1');
}
```
Requires `"lib": ["esnext.disposable"]` in tsconfig. Check target runtime support — Chrome 134+, not yet cross-browser.
## Error Handling
### Error Cause Chains
Wrap errors with context using the `cause` option:
```typescript
try {
const data = JSON.parse(raw);
} catch (err) {
throw new Error('Failed to parse config', { cause: err });
}
```
### Result Pattern
Prefer discriminated union results over thrown exceptions for expected failure cases:
```typescript
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function fetchUser(id: string): Promise<Result<User>> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) return { ok: false, error: new Error(`HTTP ${res.status}`) };
return { ok: true, value: await res.json() };
}
```
Reserve `throw` for truly exceptional / unrecoverable situations. Use `Result` for operations with expected failure modes (network requests, parsing, validation).
## Module Patterns
### ESM Conventions
- Use `"type": "module"` in `package.json`
- Prefer named exports over default exports for discoverability and refactoring
- Use the `"exports"` field to define the package's public API
- Avoid barrel files (`index.ts` re-exporting everything) in application code — they degrade tree-shaking, slow builds, and risk circular dependencies
- Prefer direct imports: `import { thing } from './utils/thing.js'`
- Use `import type` (or `verbatimModuleSyntax`) for type-only imports
### Class Patterns
Use private fields (`#field`) and accessor keywords:
```typescript
class Elevator {
#currentFloor: number;
#destination: number | null = null;
constructor(startFloor: number) {
this.#currentFloor = startFloor;
}
get currentFloor(): number {
return this.#currentFloor;
}
}
```
## TypeScript 7 / Compiler Rewrite
The TypeScript compiler is Related in Productivity
gitea-workflow
IncludedOrchestrate agile development workflows for Gitea repositories using the tea CLI. Use when working with Gitea-hosted repos and asking to 'run the workflow', 'continue working', 'what's next', 'complete the task cycle', 'start my day', 'end the sprint', 'implement the next task', or wanting guided step-by-step development assistance. Keywords: workflow, orchestrate, agile, task cycle, sprint, daily, implement, review, PR, standup, retrospective, gitea, tea.
microsoft-graph-gateway
IncludedRoute Microsoft Graph work in this workspace. Use when users want to read or write Outlook mail, calendar events, contacts, OneDrive or SharePoint files, Teams, Planner, To Do, users, groups, directory data, or arbitrary Microsoft Graph endpoints from VS Code. Prefer WorkIQ for common read scenarios. Use Microsoft Graph for write actions and gap-read scenarios that need exact Graph properties, filters, permissions, or endpoints.
copilotkit
IncludedUse when building with CopilotKit — setup, development, integrations, debugging, upgrading, or contributing. Routes to the appropriate specialized skill based on the task.
wordly-wisdom
IncludedProvides calibrated decision analysis using Charlie Munger-style multiple mental models, inversion, incentive mapping, circle-of-competence checks, misjudgment audits, second-order effects, and forecast updates. Use when the user asks for an oracle take, a hard call, a decision memo, a premortem, an outside view, a red-team, a sanity-check, what am I missing, think this through, or wants a strategy, hire, investment, plan, product, partnership, or major life choice analysed. Avoid for simple factual lookups or time-sensitive legal, medical, or market questions without fresh evidence.
swain-session
IncludedSession management and project status dashboard. Owns the full session lifecycle (start/work/close/resume), focus lane, bookmarks, worktree detection, and tab naming. Also serves as the project status dashboard — shows active epics, progress, actionable next steps, blocked items, tasks, GitHub issues, and recommendations. Worktree creation is deferred to swain-do task dispatch (SPEC-195). Triggers on: 'session', 'status', 'what's next', 'dashboard', 'overview', 'where are we', 'what should I work on', 'show me priorities', 'bookmark', 'focus on', 'session info'.
gandi
IncludedComprehensive Gandi domain registrar integration for domain and DNS management. Register and manage domains, create/update/delete DNS records (A, AAAA, CNAME, MX, TXT, SRV, and more), configure email forwarding and aliases, check SSL certificate status, create DNS snapshots for safe rollback, bulk update zone files, and monitor domain expiration. Supports multi-domain management, zone file import/export, and automated DNS backups. Includes both read-only and destructive operations with safety controls.