# Typescript Idioms

> TypeScript Idioms and Patterns

- Skill: `irahardianto/typescript-idioms` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add irahardianto/typescript-idioms`
- Raw SKILL.md: https://api.skillmd.com/api/skills/irahardianto/typescript-idioms/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: irahardianto (https://skillmd.com/u/irahardianto)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/irahardianto/typescript-idioms

---


## TypeScript Idioms and Patterns

TS type system = documentation + test + specification. Encode domain invariants so invalid states are unrepresentable. Lean into the compiler.

> Scope: TS-specific type system and language idioms. Quality commands: GEMINI.md § Code Completion Mandate. Logging: `@.gemini/skills/logging-and-observability-principles/SKILL.md`.

### Framework Detection

When working on `.ts`/`.tsx` files, detect the project framework from imports and load the corresponding idiom skill:

| Import Pattern | Framework | Load Skill |
|---|---|---|
| `from 'vue'`, `from 'pinia'`, `from 'vue-router'`, `from '@vueuse/*'` | Vue 3 | `@.gemini/skills/vue-idioms/SKILL.md` |
| `from 'react'`, `from 'react-dom'`, `from '@reduxjs/*'`, `from 'zustand'` | React | `@.gemini/skills/react-idioms/SKILL.md` |
| `from '@angular/core'`, `from '@angular/*'` | Angular | `@.gemini/skills/angular-idioms/SKILL.md` |
| `from 'next'`, `from 'next/*'` | Next.js | `@.gemini/skills/nextjs-idioms/SKILL.md` |

Framework-specific file types (`.vue`, `.jsx`) auto-activate their skills directly. This table covers `.ts`/`.tsx` files that use framework APIs (composables, hooks, stores, services).

### Strict Mode — Non-Negotiable

```json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}
```

Never disable per-file without `// STRICT-DISABLE:` rationale comment.

### Type System

1. **`unknown` over `any` — always:**
   ```typescript
   // ✅ Forces narrowing
   function parse(data: unknown): User {
       if (!isUser(data)) throw new Error('Invalid user shape');
       return data;
   }

   // ❌ Disables type checker
   function parse(data: any): User { return data; }
   ```

2. **`readonly` for immutability:**
   ```typescript
   interface TaskState {
       readonly id: string;
       readonly items: readonly Task[];
   }

   function process(tasks: readonly Task[]): Summary { ... }
   ```

3. **Discriminated unions for state machines:**
   ```typescript
   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;
       }
   }
   ```

4. **Const assertions for literal types:**
   ```typescript
   const ROLES = ['admin', 'editor', 'viewer'] as const;
   type Role = typeof ROLES[number]; // 'admin' | 'editor' | 'viewer'
   ```

5. **Type guards over `as` casts:**
   ```typescript
   // ✅ Safe narrowing
   function isError(value: unknown): value is Error {
       return value instanceof Error;
   }

   // ❌ Bypasses type checker
   const err = value as Error;
   ```

6. **Never non-null assertion `!` in production:**
   ```typescript
   // ❌ Hides null/undefined bug
   const name = user!.profile!.name;

   // ✅ Explicit handling
   const name = user?.profile?.name ?? 'Anonymous';
   ```

7. **`satisfies` for type-checked literals (TS 4.9+):**
   ```typescript
   const config = {
       endpoint: '/api/tasks',
       retries: 3,
   } satisfies ApiConfig;
   // config.retries typed as `3` (literal), not `number`
   ```

8. **`interface` for object shapes, `type` for unions/primitives:**
   ```typescript
   // ✅ interface — object shapes, extendable, better error messages
   interface User {
       id: string;
       name: string;
       role: Role;
   }

   // ✅ type — unions, intersections, primitives, mapped types
   type Role = 'admin' | 'editor' | 'viewer';
   type Nullable<T> = T | null;
   type EventHandler = (event: Event) => void;
   ```
   Why `interface` for shapes: TypeScript surfaces the interface name in error messages (not an expanded inline blob), IDE hover shows the name not the expansion, and declaration merging enables incremental extension. For AI agents specifically, `interface` produces more predictable, scannable type diagnostics.

### Null Safety

1. **`??` over `||` for defaults** — `??` only falls back for null/undefined, `||` also for 0, '', false.
   ```typescript
   const count = input.count ?? 0;  // ✅
   const count = input.count || 0;  // ❌
   ```

2. **Optional chaining `?.`** for safe navigation: `user?.address?.city`

3. **`undefined` = absence, `null` = intentionally empty (JSON APIs).**

### Async/Await

> General async: GEMINI.md § Concurrency and Threading Mandate. TS-specific below.

1. **Always `await` or handle Promises — no floating promises:**
   ```typescript
   // ❌ Fire-and-forget — errors swallowed
   sendEmail(user);

   // ✅ Awaited
   await sendEmail(user);

   // ✅ Intentional fire-and-forget
   void sendEmail(user); // logs errors internally
   ```

2. **`Promise.all` for concurrent independent ops:**
   ```typescript
   const [user, tasks] = await Promise.all([getUser(id), getTasks(id)]);
   ```

3. **`Promise.allSettled` for partial failure tolerance:**
   ```typescript
   const results = await Promise.allSettled(notifications.map(send));
   const failed = results.filter(r => r.status === 'rejected');
   ```

4. **Never mix async/await with raw `.then()/.catch()` in same fn.**

### Runtime Validation at Boundaries

All data crossing system boundary validated at runtime, not just typed.

```typescript
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 validation at API ingress/egress.
- Never use `as` as substitute for runtime validation.
- Validate on ingress; trust validated types thereafter.

### Centralized HTTP Client

All outbound HTTP MUST go through shared API client utility. No direct `fetch()`/`axios()` in feature code.

```typescript
// ❌ Bypass: no auth, no correlation-ID, no logging
const res = await fetch('/api/tasks');

// ✅ Shared client
import { apiFetch } from '@/infrastructure/apiFetch';
const res = await apiFetch('/api/tasks');
```

Why: consistent auth injection, correlation-ID propagation, centralized error normalization, single retry/timeout/logging point.

Exception: centralized client itself may use raw fetch internally.

> Direct `fetch`/`axios` outside shared client = `[INT]` audit finding.

### Module and Export Patterns

1. **Named exports over default:**
   ```typescript
   // ✅ Explicit, refactor-safe, IDE-friendly
   export function createTask() { ... }
   export type { Task };

   // ❌ Ambiguous import names
   export default function createTask() { ... }
   ```

2. **Avoid barrel re-exports creating circular deps.** Feature `index.ts` only for public API.

3. **Import type separately:**
   ```typescript
   import type { Task } from './types';
   ```

### Testing

> Test naming, file conventions, pyramid: GEMINI.md § Testing Strategy. TS-specific tooling below.

1. **Type mocks with Vitest types — never `as any`:**
   ```typescript
   import { vi } from 'vitest';
   import type { MockedObject } from 'vitest';

   const mockStore: MockedObject<TaskStore> = {
       create: vi.fn(),
       getById: vi.fn(),
   };
   ```

2. **Assert error types, not just messages:**
   ```typescript
   await expect(service.create(invalid)).rejects.toThrow(ZodError);
   ```

3. **`satisfies` for type-checked fixtures:**
   ```typescript
   const fixture = {
       id: 'abc', title: 'Test task'
   } satisfies Task;
   ```

### Formatting and Static Analysis

| Tool | Purpose | Notes |
|---|---|---|
| `vue-tsc --noEmit` | Full type checking (incl `.vue`) | `tsc --noEmit` for non-Vue |
| `eslint` | Lint + style | Use `@typescript-eslint/recommended-type-checked` |
| `prettier` | Canonical formatting | Non-negotiable |
| `npm audit` / `pnpm audit` | Dependency CVE scanning | Fail on high severity |

See GEMINI.md § Code Completion Mandate for exact commands.

### Related
- Code Idioms and Conventions GEMINI.md § Code Idioms and Conventions
- Vue Idioms and Patterns @.gemini/skills/vue-idioms/SKILL.md
- Testing Strategy GEMINI.md § Testing Strategy
- Error Handling Principles GEMINI.md § Error Handling Principles
- Concurrency and Threading Mandate GEMINI.md § Concurrency and Threading Mandate
- Security Principles GEMINI.md § Security Principles
- Dependency Management Principles @.gemini/skills/dependency-management-principles/SKILL.md

