- Add principle 27: 'Language-Agnostic Auto-Trigger' to engineering-principles - Remove hardcoded English-only keyword lists from all 26 skill descriptions - Replace with concept-based detection: triggers from code context, project files, and file types regardless of spoken language - Update detect-project.sh hook output with language-agnostic message
5.7 KiB
5.7 KiB
name, description
| name | description |
|---|---|
| typescript | TypeScript best practices — strict mode, type patterns, generics, module system, async patterns, and project organization. Use when writing TypeScript code, configuring tsconfig. Triggers when working with this language's files, regardless of spoken language." |
TypeScript Best Practices
Configuration
// tsconfig.json — strict mode is non-negotiable
{
"compilerOptions": {
"strict": true, // Enable all strict checks
"noUncheckedIndexedAccess": true, // Accessing arrays/objects is safe
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"esModuleInterop": true,
"moduleResolution": "bundler", // or "node16" for ESM
"target": "ESNext",
"isolatedModules": true,
"skipLibCheck": true // Fast, skip node_modules type check
}
}
Types vs Interfaces
// Prefer `interface` for public API contracts (extends, implements)
interface User {
id: string;
email: string;
}
// Prefer `type` for unions, intersections, complex types
type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
type Status = 'active' | 'inactive' | 'suspended';
type CreateUserInput = { name: string } & BaseInput;
Rule of thumb: interface for objects, type for everything else.
Generics — Use Intentionally
// ✅ Good — specific enough
function mapValues<K extends string, V, R>(
obj: Record<K, V>,
fn: (value: V, key: K) => R
): Record<K, R> { ... }
// ❌ Bad — over-generic, loses type info
function identity(value: any): any { ... }
// ✅ Good — preserves type
function identity<T>(value: T): T { return value; }
Async Patterns
- Top-level await — OK in ESM modules, but prefer
main()pattern. - Promise.all for parallel — don't
awaitin sequence if requests are independent. - Errors — always handle Promise rejections. No unhandled promises.
- Async iterators —
for await (const item of stream)for paginated data.
// ❌ Sequential
const a = await fetchA();
const b = await fetchB(); // waits for A
// ✅ Parallel
const [a, b] = await Promise.all([fetchA(), fetchB()]);
No any
// ❌ Never — disables all type checks
function parse(input: any): any { ... }
// ✅ Use `unknown` instead — forces type narrowing before use
function parse(input: unknown): Result<Data, ParseError> { ... }
// ✅ Use `never` for exhaustive checks
function assertNever(x: never): never { throw new Error('Unexpected: ' + x); }
Module System
- ESM only. No
require(). Useimport/export. - Named exports over default exports (better tree-shaking, rename safety).
- Barrel exports (
index.ts) — use sparingly. Can cause circular deps and slow builds. - Path aliases —
@/or~/for internal imports. Configure in tsconfig.
// Always use .js/.mjs extension for relative imports in ESM
import { User } from './user.js';
import { createOrder } from '@/use-cases/create-order.js';
Project Structure
src/
├── domain/ # Pure business logic, entities, value objects
│ ├── user.ts
│ └── order.ts
├── application/ # Use cases, ports
│ ├── create-order.ts
│ └── user-repository.ts # port (interface)
├── infrastructure/ # Adapters (DB, HTTP, queue)
│ └── postgres-user-repo.ts
├── api/ # HTTP handlers, middleware
│ ├── routes/
│ └── middleware/
├── lib/ # Shared utilities (pure, no framework deps)
└── index.ts # Composition root, entry point
Enum vs Union
// ❌ Enums — runtime overhead, not tree-shakeable, const enum has issues
enum Status { Active, Inactive }
// ✅ Union types — zero-cost, works everywhere
type Status = 'active' | 'inactive';
// ✅ Const objects with `as const` — when you need both type and runtime
const STATUS = { ACTIVE: 'active', INACTIVE: 'inactive' } as const;
type Status = (typeof STATUS)[keyof typeof STATUS];
Branded Types for IDs
// Type-safe IDs — prevents mixing up different entity IDs
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function getUser(id: UserId) { ... }
function getOrder(id: OrderId) { ... }
getUser('abc' as UserId); // OK
getUser(orderId); // ❌ Type error — OrderId not assignable to UserId
Error Handling
// Domain errors as discriminated unions
type CreateUserError = ValidationError | DuplicateEmailError | DatabaseError;
type ValidationError = { kind: 'validation'; field: string; message: string };
type DuplicateEmailError = { kind: 'duplicate_email'; email: string };
function createUser(input: CreateUserInput): Result<User, CreateUserError> { ... }
// Use branded Result in use cases, throw for unexpected infrastructure errors
class DatabaseError extends Error { constructor(cause: unknown) { super(); this.cause = cause; } }
Testing
- Vitest over Jest (faster, ESM-native, TypeScript-native).
vi.fn()for mocks. Prefer fakes (in-memory implementations).- Cover 'as const',
satisfies,z.inferpatterns — they are compile-time only.
Anti-patterns
- ❌
any— disables the type system entirely - ❌
astype assertions (value as Type) — use proper narrowing or Zod parsing - ❌
!non-null assertion (user!.name) — defeats strict null checks - ❌ Namespace — use ES modules instead
- ❌
Functiontype — use typed function signature(args: Args) => Result - ❌ Optional chaining chains —
a?.b?.c?.dis fragile. Narrow earlier.