| name | typescript-idioms |
| description | TypeScript strict mode, type narrowing, Zod validation, vitest, ESLint flat config. |
| paths | ["**/*.ts","**/*.tsx","**/tsconfig.json"] |
TypeScript Idioms and Patterns
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 in this skill. For quality commands, see code-idioms-and-conventions.md. For logging library, see @.agents/skills/logging-implementation/SKILL.md.
Strict Mode — Non-Negotiable
Always enable strict mode in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
These flags catch the majority of runtime errors at compile time. Never disable them on a per-file basis without a // STRICT-DISABLE: comment explaining the rationale.
Type System Idioms
-
unknown over any — always
function parse(data: unknown): User {
if (!isUser(data)) throw new Error('Invalid user shape');
return data;
}
function parse(data: any): User { return data; }
-
Use readonly to enforce immutability at compile time
interface TaskState {
readonly id: string;
readonly items: readonly Task[];
}
function process(tasks: readonly Task[]): Summary { ... }
-
Discriminated unions for type-safe state machines
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function render(state: AsyncState<User>): string {
switch (state.status) {
case 'idle': return 'Waiting...';
case 'loading': return 'Loading...';
case 'success': return state.data.name;
case 'error': return state.error.message;
}
}
-
Const assertions for literal types
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = typeof ROLES[number];
-
Type narrowing — use type guards instead of as casts
function isError(value: unknown): value is Error {
return value instanceof Error;
}
const err = value as Error;
-
Never use non-null assertion ! in production code
const name = user!.profile!.name;
const name = user?.profile?.name ?? 'Anonymous';
-
satisfies operator for type-checked object literals (TS 4.9+)
const config = {
endpoint: '/api/tasks',
retries: 3,
} satisfies ApiConfig;
Null Safety
-
Prefer ?? (nullish coalescing) over || for default values
const count = input.count ?? 0;
const count = input.count || 0;
-
Use optional chaining ?. for safe navigation
const city = user?.address?.city;
-
Distinguish undefined (absence) from null (explicit empty)
- Use
undefined for optional fields
- Use
null only when you need to represent "intentionally empty" on the wire (JSON APIs)
Async/Await
For general async patterns (when to use concurrency), see core-design-principles.md § Concurrency. This section covers TypeScript-specific async idioms.
-
Always await or handle returned Promises — no floating promises
sendEmail(user);
await sendEmail(user);
void sendEmail(user);
-
Use Promise.all for concurrent independent operations
const [user, tasks] = await Promise.all([getUser(id), getTasks(id)]);
const user = await getUser(id);
const tasks = await getTasks(id);
-
Use Promise.allSettled when partial failure is acceptable
const results = await Promise.allSettled(notifications.map(send));
const failed = results.filter(r => r.status === 'rejected');
-
Never mix async/await with raw .then()/.catch() chains in the same function
Runtime Validation at Boundaries
All data crossing a system boundary must be validated at runtime, not just typed.
import { z } from 'zod';
const CreateTaskSchema = z.object({
title: z.string().min(1).max(200),
priority: z.enum(['low', 'medium', 'high']),
dueDate: z.string().datetime().optional(),
});
type CreateTaskRequest = z.infer<typeof CreateTaskSchema>;
function parseCreateTask(body: unknown): CreateTaskRequest {
return CreateTaskSchema.parse(body);
}
- Use
zod for runtime schema validation at API ingress and external API egress
- Never use TypeScript's
as operator as a substitute for runtime validation
- Validate on ingress; trust validated types thereafter
Centralized HTTP Client
All outbound HTTP calls MUST go through the project's single, shared API client utility.
Do not call fetch() or axios() directly in feature code. Route every request through the centralized client (e.g., apiFetch, apiClient, or equivalent).
Why this matters:
- Consistent auth header injection (token is attached in one place)
- Correlation-ID propagation — every request carries a traceable ID
- Centralized error normalization — uniform error shapes for all API failures
- Single place to add retries, timeouts, and request logging
const res = await fetch('/api/tasks');
import { apiFetch } from '@/infrastructure/apiFetch';
const res = await apiFetch('/api/tasks');
Exception: The centralized client itself may use raw fetch or axios internally — that is its implementation detail, not a bypass.
The audit's Integration Contracts dimension (Phase 1.5, Dimension A) checks compliance with this rule. Any direct fetch/axios call outside the shared client is a [INT] finding.
Module and Export Patterns
-
Prefer named exports over default exports
export function createTask() { ... }
export type { Task };
export default function createTask() { ... }
-
Avoid barrel re-exports that create circular dependency risk
- Use feature
index.ts files only for the public API of a feature, not as catch-all re-exports
-
Import type separately to avoid bundling runtime artifacts
import type { Task } from './types';
Testing
Test naming, file conventions, and pyramid proportions are defined in testing-strategy.md. This section covers TypeScript-specific tooling.
-
Type your mocks with Vitest types — never use as any in test doubles
import { vi } from 'vitest';
import type { MockedObject } from 'vitest';
const mockStore: MockedObject<TaskStore> = {
create: vi.fn(),
getById: vi.fn(),
};
-
Assert on error types, not just error messages
await expect(service.create(invalid)).rejects.toThrow(ZodError);
-
Use satisfies operator in tests for type-checked fixtures
const fixture = {
id: 'abc', title: 'Test task'
} satisfies Task;
Formatting and Static Analysis
| Tool | Purpose | Notes |
|---|
vue-tsc --noEmit | Full type checking (incl. .vue files) | Must pass zero errors; use tsc --noEmit for non-Vue TS projects |
eslint | Lint rules + style | Use @typescript-eslint/recommended-type-checked |
prettier | Canonical formatting | Non-negotiable |
npm audit / pnpm audit | Dependency CVE scanning | Run in CI; fail on high severity |
See code-idioms-and-conventions.md for the exact commands to run before committing.
Related Principles
- Code Idioms and Conventions @code-idioms-and-conventions.md
- Vue Idioms and Patterns @.agents/skills/vue-idioms/SKILL.md
- Architectural Patterns — Testability-First Design @architectural-pattern.md
- Testing Strategy @testing-strategy.md
- Error Handling Principles @error-handling-principles.md
- Core Design Principles § Concurrency @core-design-principles.md
- Security Principles @security-principles.md
- Dependency Management Principles @dependency-management-principles.md
- Logging and Observability Principles @.agents/skills/logging-implementation/SKILL.md