| name | typescript-idioms |
| description | TypeScript strict mode, type narrowing, Zod validation, vitest, ESLint flat config. |
| paths | ["**/*.ts","**/*.tsx","**/tsconfig.json"] |
Core Philosophy
TypeScript's type system is your documentation, your test, and your specification — all at once. Make the type system encode the invariants of your domain so that invalid states are unrepresentable. Lean into the compiler.
Scope: This file covers TypeScript-specific type system and language idioms. For framework-specific patterns, see the respective idiom skill (Vue, React, Angular, Next.js, Hono). For file layout, see references/project-structure.md. For detailed safety, SAST patterns, and performance patterns, see references/ts-patterns-and-anti-patterns.md. For quality commands, see @.agents/rules/code-idioms-and-conventions.md. For logging library, see @.agents/skills/logging-implementation/SKILL.md.
Loading guards:
- Plain JavaScript (no
tsconfig.json): load @.agents/skills/javascript-idioms/SKILL.md instead — this skill assumes strict-mode TS.
- Hono backend:
hono has no repo-level marker file, so it is not auto-detected. If hono appears in package.json dependencies, co-load @.agents/skills/hono-idioms/SKILL.md alongside this skill.
- Test-file naming diverges by framework: see
references/project-structure.md § Test Organization for the reconciliation rule before creating test files.
When to Load References
Load these before writing code in the matching context — not after.
| Situation | Reference to Load |
|---|
| Starting a new project or setting up file layout | references/project-structure.md |
| Choosing packages, tsconfig template, or vitest config | references/recommended-dependencies.md |
| Writing code that handles user input, async operations, or I/O | references/ts-patterns-and-anti-patterns.md |
| Defining Zod schemas or validating API/env boundaries | references/zod-patterns.md |
Toolchain and Runtime
- Default to latest Node.js LTS. As of July 2026, Node.js 24 LTS with TypeScript 5.8+
- ESM over CJS for all new projects
tsx for development execution (replaces ts-node)
- Key version milestones: TS 5.0+ decorators, TS 5.4+ NoInfer, TS 5.5+ inferred type predicates
Strict Mode — Non-Negotiable
All TypeScript projects MUST have strict mode enabled. If a project does not have these settings, fix tsconfig.json before proceeding.
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
Type System Idioms
-
Use unknown instead of any
function processPayload(payload: any) {
console.log(payload.id);
}
function processPayload(payload: unknown) {
if (typeof payload === 'object' && payload !== null && 'id' in payload) {
console.log(payload.id);
}
}
-
Discriminated Unions for state machines
type State = {
status: 'loading' | 'success' | 'error';
data?: string;
error?: Error;
};
type State =
| { status: 'loading' }
| { status: 'success'; data: string }
| { : ; : };
Null Safety
- Enable
strictNullChecks (always).
- Optional Chaining (
?.) over explicit checks
const city = user && user.address && user.address.city;
const city = user?.address?.city;
- Nullish Coalescing (
??) over Logical OR (||)
const count = input.count || 10;
const count = input.count ?? 10;
- Explicit Resource Management (
using declarations — TypeScript 5.2+)
Requires lib: ["es2022"] or higher in tsconfig.json. Available in all Node.js 24 LTS projects.
{
using file = await openFile('data.csv');
}
Error Handling
-
Always throw Error instances, never primitives
throw 'Something went wrong';
throw { message: 'fail' };
throw new Error('Something went wrong');
-
Custom error classes for domain errors
export class NotFoundError extends Error {
constructor(public readonly resource: string, public readonly id: string) {
super(`${resource} not found: ${id}`);
this.name = 'NotFoundError';
}
}
-
Type-safe error narrowing
try {
await api.createUser(data);
} catch (err) {
console.log(err.message);
if (err instanceof NotFoundError) {
.(err., err.);
} (err ) {
.(err.);
}
}
Async/Await
-
Always use async/await over raw Promises.
-
Use Promise.all for parallel operations.
const users = await getUsers();
const posts = await getPosts();
const [users, posts] = await Promise.all([getUsers(), getPosts()]);
-
Use Promise.allSettled when some can fail.
-
Avoid .then().catch() chains.
-
Abort long-running operations with AbortController
const controller = new AbortController();
const response = await fetch(url, { signal: controller.signal });
controller.abort();
-
Never use async callbacks in Array.forEach
items.forEach(async (item) => {
await process(item);
});
for (const item of items) {
await process(item);
}
.(items.( (item)));
Runtime Validation at Boundaries
TypeScript types do not exist at runtime. Any data crossing an I/O boundary must be validated.
- Use Zod for all schema validation.
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(2),
age: z.number().int().nonnegative(),
});
type User = z.infer<typeof UserSchema>;
function parseUser(data: unknown): User {
return UserSchema.parse(data);
}
For advanced Zod patterns (transforms, discriminated unions, branded types, error formatting), see references/zod-patterns.md.
Iteration and Collections
-
Use Map/Set over plain objects for dynamic keys
const cache: Record<string, User> = {};
const cache = new Map<string, User>();
-
Use structuredClone() for deep copies (not JSON round-trip)
const copy = JSON.parse(JSON.stringify(original));
const copy = structuredClone(original);
-
Prefer immutable array methods (ES2023+)
const sorted = arr.sort((a, b) => a - b);
const reversed = arr.reverse();
const sorted = arr.toSorted((a, b) => a - b);
const reversed = arr.toReversed();
const withReplacement = arr.with(, );
Centralized HTTP Client
Never use raw fetch spread throughout the codebase. Centralize it to handle tokens, retries, and errors.
Testability requirement: Always define an interface first. The concrete class implements it.
This lets tests inject a FakeHttpClient without network calls.
export interface HttpClient {
get<T>(path: string, schema: z.ZodType<T>): Promise<T>;
post<T>(path: string, body: unknown, schema: z.ZodType<T>): Promise<T>;
delete(path: string): Promise<void>;
}
export class ApiClient implements HttpClient {
constructor(private readonly baseUrl: string) {}
async get<T>(path: string, schema: z.ZodType<T>): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) throw (res., res.);
: = res.();
schema.(data);
}
post<T>(: , : , : z.<T>): <T> {
res = (, {
: ,
: { : },
: .(body),
: .(),
});
(!res.) (res., res.);
: = res.();
schema.(data);
}
(: ): <> {
res = (, {
: ,
: .(),
});
(!res.) (res., res.);
}
}
{
: { : ; : }[] = [];
responses = <, >();
(: , : ): {
..(path, response);
}
get<T>(: , : z.<T>): <T> {
..({ : , path });
schema.(..(path));
}
post<T>(: , : , : z.<T>): <T> {
..({ : , path });
schema.(..(path));
}
(: ): <> {
..({ : , path });
}
}
Idiomatic Patterns
-
Parameter object over positional arguments
function createUser(name: string, email: string, isAdmin: boolean, isActive: boolean) {}
interface CreateUserParams {
name: string;
email: string;
isAdmin: boolean;
isActive: boolean;
}
function createUser(params: CreateUserParams) {}
-
Branded/Opaque types for domain primitives — see Type System Idioms §6. Never pass bare string or number for domain IDs.
-
Discriminated unions over inheritance — prefer union types with a type or kind discriminant over class hierarchies. Invalid states become compile errors.
-
Parse, don't validate — convert raw input into typed, validated domain objects at the boundary. Downstream code works with the typed form and never re-validates.
function processOrder(orderId: string) {
if (!isValidUuid(orderId)) ();
}
orderId = .(rawInput);
(orderId);
Module and Export Patterns
- Avoid
export default. Use named exports for better refactoring and intellisense.
- Use barrel files (
index.ts) sparingly. They can cause circular dependencies.
- Use type-only imports.
import type { User } from './types';
import { parseUser } from './parser';
ESLint Suppression Policy
NEVER suppress these rules — they signal structural problems that must be fixed:
| Rule | What It Signals | What To Do Instead |
|---|
@typescript-eslint/no-explicit-any | Type safety disabled | Use unknown and narrow |
@typescript-eslint/no-floating-promises | Unhandled async operation | Add await or void |
@typescript-eslint/no-unsafe-assignment | Unsafe type flow | Type the source properly |
@typescript-eslint/no-unnecessary-condition | Dead code or logic bug | Remove the condition |
complexity | Function too complex | Decompose into smaller functions |
Acceptable suppressions (with mandatory // SUPPRESS: comment):
| Rule | When Acceptable |
|---|
@typescript-eslint/no-non-null-assertion | After runtime validation proves non-null |
@typescript-eslint/ban-ts-comment | @ts-expect-error with explanation (never @ts-ignore) |
no-console | In CLI tools or development scripts |
Rule of thumb: If you're about to write // eslint-disable, stop and ask: "Am I suppressing a real design problem?" If yes, fix the design.
Testing
-
Use Vitest over Jest. It's faster, ESM-native, and requires zero config for TS.
-
AAA Pattern (Arrange, Act, Assert).
-
Test behavior, not implementation.
-
Test async errors by type, not message
-
Use vi.spyOn for interaction verification
-
Use satisfies for type-checked test fixtures
const mockUser = { id: '1', name: 'Test' } satisfies Partial<User>;
-
Test coverage is non-negotiable for new code:
- Every new exported function and class method MUST have at least one test
- Every new branch (
if/else, switch arm, error path) MUST be exercised
- When modifying existing code, add tests for the modified paths if none exist
- Never leave a function untested with the intent to "add tests later"
- Use
@vitest/coverage-v8 to verify coverage locally before committing
vitest run --coverage
vitest run --coverage --coverage.thresholds.lines=80
-
Test double selection — choose the right tool:
| Approach | When to Use |
|---|
| Hand-written fake (implement interface) | Simple interface, few methods, need stateful behavior |
vi.fn() / vi.spyOn() | Verify call counts, argument matching |
msw (Mock Service Worker) | HTTP boundary mocking — intercepts at network level |
Parameterized it.each / test.each |
Feedback Loop — Development Workflow
tsc --noEmit is the TypeScript equivalent of Rust's cargo check — type-checks without producing output. It is the fastest possible feedback during TDD cycles.
| Phase | Command | Purpose |
|---|
| TDD / rapid iteration | tsc --noEmit | Type-check only, no emit — fastest loop |
| Pre-commit | eslint . | Static analysis — must pass with zero warnings |
| Pre-commit | prettier --write . | Formatting — non-negotiable, always run |
| Pre-commit | vitest run | Unit tests — must all pass |
| Coverage verification | vitest run --coverage | Verify before merging |
| Unused dep audit | knip | Run before releases |
Rules:
- Never run a full
tsc build during TDD cycles — tsc --noEmit is sufficient and significantly faster.
eslint . must pass with zero warnings before any commit. Warnings are treated as errors.
prettier --write . is non-negotiable — all code must be formatted before committing.
- If
knip reports unused exports or dependencies, remove them before the release.
Documentation
Document all exported items:
- Every exported function, class, type, and interface MUST have a JSDoc comment
- At minimum: one-line summary. For complex items: summary +
@param + @returns + @throws
- Document the why for non-obvious design decisions, not the what
export function parseUserToken(token: string): UserId { ... }
export function parseUserToken(token: string): UserId { ... }
Dependency Management
- Minimize dependency count — each dependency is an attack surface and bundle-size cost
- Audit regularly — run
npm audit or pnpm audit in CI
- Pin major versions in
package.json with ^ for libraries (^3.0.0)
- Always commit the lockfile (
package-lock.json or pnpm-lock.yaml) — for both apps and libraries
- Check for unused dependencies with
knip before releases
- Prefer native APIs over packages when the native alternative is stable:
crypto.randomUUID() over uuid package
structuredClone() over lodash deep clone
Array.toSorted() over lodash sort
Object.groupBy() over lodash groupBy
- Never import entire utility libraries when only one function is needed — use subpath imports or native alternatives
For the full curated dependency list with versions, see references/recommended-dependencies.md.
Configuration and Environment
-
Never scatter process.env calls throughout the codebase
const port = process.env.PORT || '3000';
const dbUrl = process.env.DATABASE_URL;
import { z } from 'zod';
const EnvSchema = z.object({
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});
export const env = EnvSchema.parse(process.env);
-
Fail fast on missing required config at boot, not at first use
Safety, Security, and Performance
For type coercion traps, prototype pollution, scope bugs, security vulnerabilities, collection pitfalls, and performance invariants, see references/ts-patterns-and-anti-patterns.md. Load it before writing any code handling user input, async operations, or I/O.
For performance patterns, see perf-optimization skill.
Related
- Error Handling Principles @.agents/rules/error-handling-principles.md
- Security Principles @.agents/rules/security-principles.md
- Architectural Patterns — Testability-First Design @.agents/rules/architectural-pattern.md
- Concurrency and Threading Principles @.agents/rules/concurrency-and-threading-principles.md
- Core Design Principles @.agents/rules/core-design-principles.md
- Performance Optimization Principles @.agents/rules/performance-optimization-principles.md
- Resource and Memory Management Principles @.agents/rules/resources-and-memory-management-principles.md
- Security Mandate @.agents/rules/security-mandate.md
- Code Idioms and Conventions @.agents/rules/code-idioms-and-conventions.md
- Testing Strategy @.agents/rules/testing-strategy.md
- Logging and Observability Mandate @.agents/rules/logging-and-observability-mandate.md
- Dependency Management Principles @.agents/rules/dependency-management-principles.md
- Logging Implementation @.agents/skills/logging-implementation/SKILL.md
- Vue Idioms @.agents/skills/vue-idioms/SKILL.md
- React Idioms @.agents/skills/react-idioms/SKILL.md
- Hono Idioms @.agents/skills/hono-idioms/SKILL.md
- Next.js Idioms @.agents/skills/nextjs-idioms/SKILL.md
- Angular Idioms @.agents/skills/angular-idioms/SKILL.md
- Testability Patterns @.agents/skills/testability-patterns/SKILL.md