TypeScript Development
Type-safe code = compile-time errors = runtime confidence.
<when_to_use>
- Writing new TypeScript code
- Eliminating
anytypes - Using modern TypeScript 5.5+ features
- Validating API inputs/outputs with Zod
- Implementing Result types and discriminated unions
- Creating branded types for domain concepts
NOT for: runtime-only logic unrelated to types, non-TypeScript projects
</when_to_use>
<config>tsconfig.json strict settings:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"skipLibCheck": false
}
}
Version requirements: TS 5.2+ (using), TS 5.4+ (NoInfer), TS 5.5+ (inferred predicates)
Core Patterns
<eliminating_any>
any defeats the type system. Use unknown + guards.
// ❌ NEVER
function process(data: any) { return data.value; }
// ✅ ALWAYS
function process(data: unknown): string {
if (!hasValue(data)) throw new TypeError('Invalid');
return data.value.toString();
}
function hasValue(v: unknown): v is { value: unknown } {
return typeof v === 'object' && v !== null && 'value' in v;
}
Validate at boundaries:
async function fetchUser(id: string): Promise<User> {
const data: unknown = await fetch(`/api/users/${id}`).then(r => r.json());
return UserSchema.parse(data);
}
</eliminating_any>
<result_types>
Exceptions hide errors from types. Result makes them explicit.
type Result<T, E = Error> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: E };
type UserError =
| { readonly type: 'not-found'; readonly id: string }
| { readonly type: 'network'; readonly message: string };
async function getUser(id: string): Promise<Result<User, UserError>> {
try {
const response = await fetch(`/api/users/${id}`);
if (response.status === 404)
return { ok: false, error: { type: 'not-found', id } };
if (!response.ok)
return { ok: false, error: { type: 'network', message: response.statusText } };
return { ok: true, value: await response.json() };
} catch (e) {
return { ok: false, error: { type: 'network', message: String(e) } };
}
}
// Caller must handle
const result = await getUser(id);
if (!result.ok) {
switch (result.error.type) {
case 'not-found': return showNotFound(result.error.id);
case 'network': return showError(result.error.message);
}
}
return renderUser(result.value);
See result-pattern.md for utilities (map, flatMap, combine).
</result_types>
<discriminated_unions>
Prevent illegal state combinations.
// ❌ Allows { status: 'loading', data: user, error: 'Failed' }
type Request = { status: 'idle'|'loading'|'success'|'error'; data?: User; error?: string; };
// ✅ Only valid states
type RequestState =
| { readonly status: 'idle' }
| { readonly status: 'loading' }
| { readonly status: 'success'; readonly data: User }
| { readonly status: 'error'; readonly error: string };
function render(state: RequestState): JSX.Element {
switch (state.status) {
case 'idle': return <div>Ready</div>;
case 'loading': return <div>Loading...</div>;
case 'success': return <div>{state.data.name}</div>;
case 'error': return <div>Error: {state.error}</div>;
default: return assertNever(state);
}
}
function assertNever(value: never): never {
throw new Error(`Unhandled: ${JSON.stringify(value)}`);
}
</discriminated_unions>
<branded_types>
Prevent mixing incompatible primitives.
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [__brand]: B };
type UserId = Brand<string, 'UserId'>;
type ProductId = Brand<string, 'ProductId'>;
function createUserId(value: string): UserId {
if (!/^user-\d+$/.test(value)) throw new TypeError(`Invalid: ${value}`);
return value as UserId;
}
const userId = createUserId('user-123');
// getUser(productId); // ❌ Type error
getUser(userId); // ✅ Works
Security:
type SanitizedHtml = Brand<string, 'SanitizedHtml'>;
function sanitize(raw: string): SanitizedHtml {
return escapeHtml(raw) as SanitizedHtml;
}
function render(html: SanitizedHtml): void {
element.innerHTML = html; // Type proves sanitization
}
See branded-types.md for advanced patterns.
</branded_types>
Modern TypeScript (5.2+)
<resource_management>
using for automatic cleanup (TS 5.2+):
class DatabaseConnection implements Disposable {
[Symbol.dispose]() { this.close(); }
}
function query() {
using conn = new DatabaseConnection();
return conn.query('SELECT * FROM users');
} // Automatically closed
async function asyncWork() {
await using resource = new AsyncResource();
} // Disposed with await
Use for: connections, file handles, locks, transactions.
</resource_management>
<satisfies_operator>
Validate type without widening (TS 4.9+):
const config = {
port: 3000,
host: 'localhost'
} satisfies Record<string, string | number>;
config.port // number (not string | number)
const routes = {
home: '/',
user: '/user/:id'
} as const satisfies Record<string, string>;
type HomeRoute = typeof routes.home; // '/'
</satisfies_operator>
<const_type_parameters>
Preserve literals through generics (TS 5.0+):
function makeTuple<const T extends readonly unknown[]>(...args: T): T {
return args;
}
const result = makeTuple('a', 'b', 'c'); // ['a', 'b', 'c'] not string[]
</const_type_parameters>
<inferred_predicates>
TS 5.5+ auto-infers type predicates:
function isString(x: unknown) {
return typeof x === 'string';
}
// Inferred: (x: unknown) => x is string
const strings = values.filter(isString); // string[]
</inferred_predicates>
<template_literals>
Pattern matching at type level:
type Route = `/${string}`;
type ApiRoute = `/api/v${number}/${string}`;
type ExtractParams<T extends string> =
T extends `${string}:${infer P}/${infer R}` ? P | ExtractParams<`/${R}`>
: T extends `${string}:${infer P}` ? P : never;
type Params = ExtractParams<'/user/:id/post/:postId'>; // 'id' | 'postId'
See modern-features.md for TS 5.5-5.8.
</template_literals>
Zod Validation
Schema = runtime validation + TypeScript type.
<zod_core>
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100)
});
type User = z.infer<typeof UserSchema>;
// safeParse preferred
const result = UserSchema.safeParse(data);
if (!result.success) {
console.error(result.error.issues);
return;
}
const user = result.data;
</zod_core>
<zod_patterns>
Discriminated unions (preferred over z.union):
const ApiResponse = z.discriminatedUnion("type", [
z.object({ type: z.literal("success"), data: z.unknown() }),
z.object({ type: z.literal("error"), code: z.string(), message: z.string() })
]);
Environment variables:
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().int().positive().default(3000)
});
const env = EnvSchema.parse(process.env);
API validation (Hono):
import { zValidator } from '@hono/zod-validator';
app.post('/users', zValidator('json', UserSchema), (c) => {
const user = c.req.valid('json');
return c.json(user);
});
See:
- zod-building-blocks.md - primitives, refinements, transforms
- zod-schemas.md - composition patterns
- zod-integration.md - API/form/env integration
</zod_patterns>
Type Guards
// User-defined
function isString(v: unknown): v is string {
return typeof v === 'string';
}
// Assertion
function assertString(v: unknown): asserts v is string {
if (typeof v !== 'string') throw new TypeError('Expected string');
}
// With noUncheckedIndexedAccess
const users: User[] = getUsers();
const first = users[0]; // User | undefined
if (first !== undefined) processUser(first);
See advanced-types.md for utilities.
TSDoc
Types show structure. TSDoc shows intent. Critical for AI agents.
/**
* Authenticates user and returns session token.
* @param credentials - User login credentials
* @returns Session token valid for 24 hours
* @throws {AuthenticationError} Invalid credentials
* @example
* const token = await authenticate({ email, password });
*/
export async function authenticate(credentials: Credentials): Promise<SessionToken>;
Document: all exports, parameters with constraints, thrown errors, non-obvious returns.
See tsdoc-patterns.md for comprehensive guide.
<rules>ALWAYS:
- Strict TypeScript config enabled
- Type-only imports:
import type { User } from './types' - Const assertions for literal types
- Exhaustive matching with
assertNever - Runtime validation at boundaries (Zod)
- Branded types for domain/sensitive data
- Result types for error-prone operations
satisfiesfor literal inferenceusingfor resources with cleanup- TSDoc on all exports
NEVER:
any(useunknown+ guards)@ts-ignore(fix types or document)- TypeScript enums (use const assertions or z.enum)
- Non-null assertions
!(use guards) - Loose state (use discriminated unions)
- Hidden errors (use Result)
PREFER:
- safeParse over parse
- z.discriminatedUnion over z.union
- Inferred predicates (TS 5.5+)
- Const type parameters for literals
Type Patterns:
- result-pattern.md - Result/Either utilities
- branded-types.md - advanced branded patterns
- advanced-types.md - template literals, utilities
Modern Features:
- modern-features.md - TS 5.5-5.8
- migration-paths.md - upgrading TypeScript
Zod:
- zod-building-blocks.md - primitives, transforms
- zod-schemas.md - composition patterns
- zod-integration.md - API, forms, env
TSDoc:
- tsdoc-patterns.md - documentation patterns
Examples:
- api-response.md - end-to-end type-safe API
- form-validation.md - Zod + React Hook Form
- resource-management.md - using declarations
- state-machine.md - discriminated union patterns