Skill v1.0.1
currentAutomated scan100/1002 files
version: "1.0.1" name: howto-code-in-typescript description: Use when writing TypeScript code, reviewing TS implementations, or making decisions about type declarations, function styles, or naming conventions - comprehensive house style covering type vs interface rules, function declarations, FCIS integration, immutability patterns, and type safety enforcement user-invocable: false
TypeScript House Style
Overview
Comprehensive TypeScript coding standards emphasizing type safety, immutability, and integration with Functional Core, Imperative Shell (FCIS) pattern.
Core principles:
- Types as documentation and constraints
- Immutability by default prevents bugs
- Explicit over implicit (especially in function signatures)
- Functional Core returns Results, Imperative Shell may throw
- Configuration over decoration/magic
Quick Self-Check (Use Under Pressure)
When under deadline pressure or focused on other concerns (performance, accuracy, features), STOP and verify:
- [ ] Using
Array<T>notT[] - [ ] Using
typenotinterface(unless class contract) - [ ] Using math.js for money/currencies/complex math
- [ ] Parameters are
readonlyorReadonly<T> - [ ] Using
unknownnotany - [ ] Using
nullfor absent values (notundefined) - [ ] Using function declarations (not const arrow) for top-level functions
- [ ] Using named exports (not default exports)
- [ ] Using
===not== - [ ] Using
.sort((a, b) => a - b)for numeric arrays - [ ] Using
parseInt(x, 10)with explicit radix
Why this matters: Under pressure, you'll default to muscle memory. These checks catch the most common violations.
Type Declarations
Type vs Interface
Always use `type` except for class contracts.
// GOOD: type for object shapestype UserData = {readonly id: string;name: string;email: string | null;};// GOOD: interface for class contractinterface IUserRepository {findById(id: string): Promise<User | null>;}class UserRepository implements IUserRepository {// implementation}// BAD: interface for object shapeinterface UserData {id: string;name: string;}
Rationale: Types compose better with unions and intersections, support mapped types, and avoid declaration merging surprises. Interfaces are only for defining what a class must implement.
IMPORTANT: Even when under deadline pressure, even when focused on other concerns (financial accuracy, performance optimization, bug fixes), take 2 seconds to ask: "Is this a class contract?" If no, use type. Don't default to interface out of habit.
Naming Conventions
Type Suffixes
| Suffix | Usage | Example | |
|---|---|---|---|
FooOptions | Function parameter objects (3+ args or any optional) | ProcessUserOptions | |
FooConfig | Persistent configuration from storage | DatabaseConfig | |
FooResult | Discriminated union return types | ValidationResult | |
FooFn | Function/callback types | TransformFn<T> | |
FooProps | React component props | ButtonProps | |
FooState | State objects (component/application) | AppState |
General Casing
| Element | Convention | Example | |
|---|---|---|---|
| Variables & functions | camelCase | userName, getUser() | |
| Types & classes | PascalCase | UserData, UserService | |
| Constants | UPPER_CASE | MAX_RETRY_COUNT, API_ENDPOINT | |
| Files | kebab-case | user-service.ts, process-order.ts |
Boolean Naming
Use is/has/can/should/will prefixes. Avoid negative names.
// GOODconst isActive = true;const hasPermission = checkPermission();const canEdit = user.role === 'admin';const shouldRetry = attempts < MAX_RETRIES;const willTimeout = elapsed > threshold;// Also acceptable: adjectives for statetype User = {active: boolean;visible: boolean;disabled: boolean;};// BAD: negative namesconst isDisabled = false; // prefer isEnabledconst notReady = true; // prefer isReady
Type Suffix Details
FooOptions - Parameter Objects
Use for functions with 3+ arguments OR any optional arguments.
type ProcessUserOptions = {readonly name: string;readonly email: string;readonly age: number;readonly sendWelcome?: boolean;};// GOOD: destructure in body, not in parametersfunction processUser(options: ProcessUserOptions): void {const {name, email, age, sendWelcome = true} = options;// implementation}// BAD: inline destructuring in parametersfunction processUser({name, email, age}: {name: string, email: string, age: number}) {// causes duplication when destructuring}// BAD: not using options pattern for 3+ argsfunction processUser(name: string, email: string, age: number, sendWelcome?: boolean) {// hard to call, positional arguments}
FooResult - Discriminated Unions
Always use discriminated unions for Result types. Integrate with neverthrow.
// GOOD: discriminated union with success/errortype ValidationResult =| { success: true; data: ValidUser }| { success: false; error: ValidationError };// GOOD: use neverthrow for Result typesimport {Result, ok, err} from 'neverthrow';type ValidationError = {field: string;message: string;};function validateUser(data: Readonly<UserData>): Result<ValidUser, ValidationError> {if (!data.email) {return err({field: 'email', message: 'Email is required'});}return ok({...data, validated: true});}// Usageconst result = validateUser(userData);if (result.isOk()) {console.log(result.value); // ValidUser} else {console.error(result.error); // ValidationError}
Rule: Functional Core functions should return Result<T, E> types. Imperative Shell functions may throw exceptions for HTTP errors and similar.
Functions
Declaration Style
Use `function` declarations for top-level functions. Use arrow functions for inline callbacks.
// GOOD: function declaration for top-levelfunction processUser(data: Readonly<UserData>): ProcessResult {return {success: true, user: data};}// GOOD: arrow functions for inline callbacksconst users = rawData.map(u => transformUser(u));button.addEventListener('click', (e) => handleClick(e));fetch(url).then(data => processData(data));// BAD: const arrow for top-level functionconst processUser = (data: UserData): ProcessResult => {return {success: true, user: data};};
Rationale: Function declarations are hoisted and more visible. Arrow functions capture lexical this and are concise for callbacks.
Const Arrow Functions
Use `const foo = () => {}` declarations only for stable references.
// GOOD: stable reference for React hooksconst handleSubmit = (event: FormEvent) => {event.preventDefault();// implementation};useEffect(() => {// handleSubmit reference is stable}, [handleSubmit]);// GOOD: long event listener passed from variableconst handleComplexClick = (event: MouseEvent) => {// many lines of logic};element.addEventListener('click', handleComplexClick);// BAD: const arrow for regular top-level functionconst calculateTotal = (items: Array<Item>): number => {return items.reduce((sum, item) => sum + item.price, 0);};// GOOD: use function declarationfunction calculateTotal(items: ReadonlyArray<Item>): number {return items.reduce((sum, item) => sum + item.price, 0);}
Parameter Objects
Use parameter objects for 3+ arguments OR any optional arguments.
// GOOD: options object for 3+ argstype CreateUserOptions = {readonly name: string;readonly email: string;readonly age: number;readonly newsletter?: boolean;};function createUser(options: CreateUserOptions): User {const {name, email, age, newsletter = false} = options;// implementation}// GOOD: 2 args, but one is optional - use optionstype SendEmailOptions = {readonly to: string;readonly subject: string;readonly body?: string;};function sendEmail(options: SendEmailOptions): void {// implementation}// GOOD: 2 required args - no options neededfunction divide(numerator: number, denominator: number): number {return numerator / denominator;}
Async Functions
Always explicitly type Promise returns. Avoid async void.
// GOOD: explicit Promise return typeasync function fetchUser(id: string): Promise<User> {const response = await fetch(`/api/users/${id}`);return response.json();}// GOOD: Promise<void> for side effectsasync function saveUser(user: User): Promise<void> {await fetch('/api/users', {method: 'POST',body: JSON.stringify(user),});}// BAD: implicit return typeasync function fetchUser(id: string) {const response = await fetch(`/api/users/${id}`);return response.json();}
Prefer async/await over `.then()` chains.
// GOOD: async/awaitasync function processUserData(id: string): Promise<ProcessedUser> {const user = await fetchUser(id);const enriched = await enrichUserData(user);return transformUser(enriched);}// BAD: promise chainsfunction processUserData(id: string): Promise<ProcessedUser> {return fetchUser(id).then(user => enrichUserData(user)).then(enriched => transformUser(enriched));}
When to Use Async
Be selective with async. Not everything needs to be async. Sync code is simpler to reason about and debug.
Use async for:
- Network requests, database operations, file I/O
- Operations that benefit from concurrent execution (Promise.all)
- External service calls
Stay sync for:
- Pure calculations and transformations
- Simple data structure operations
- Code that doesn't touch external systems
// GOOD: sync for pure transformationfunction transformUser(user: User): TransformedUser {return {fullName: `${user.firstName} ${user.lastName}`,email: user.email.toLowerCase(),};}// GOOD: async for I/Oasync function loadAndTransformUser(id: string): Promise<TransformedUser> {const user = await fetchUser(id);return transformUser(user); // Sync call inside async function is fine}// BAD: unnecessary asyncasync function transformUser(user: User): Promise<TransformedUser> {return {fullName: `${user.firstName} ${user.lastName}`,email: user.email.toLowerCase(),};}
Why this matters: Async adds complexity—error propagation, cleanup, and stack traces become harder to follow. Keep the async boundary as close to the I/O as possible.
Classes
When to Use Classes
Prefer functions over classes, EXCEPT for dependency injection patterns.
// GOOD: class as dependency containerclass UserService {constructor(private readonly db: Database,private readonly logger: Logger,private readonly cache: Cache,) {}async getUser(id: string): Promise<User | null> {this.logger.info(`Fetching user ${id}`);const cached = await this.cache.get(`user:${id}`);if (cached) return cached;const user = await this.db.users.findById(id);if (user) await this.cache.set(`user:${id}`, user);return user;}}// BAD: class with no dependenciesclass MathUtils {add(a: number, b: number): number {return a + b;}}// GOOD: plain functionsfunction add(a: number, b: number): number {return a + b;}
Class Structure
Use constructor injection into private readonly fields.
// GOOD: constructor injection, private readonlyclass OrderProcessor {constructor(private readonly orderRepo: OrderRepository,private readonly paymentService: PaymentService,private readonly notifier: NotificationService,) {}async processOrder(orderId: string): Promise<void> {const order = await this.orderRepo.findById(orderId);// implementation}}// BAD: public mutable fieldsclass OrderProcessor {public orderRepo: OrderRepository;public paymentService: PaymentService;constructor(orderRepo: OrderRepository, paymentService: PaymentService) {this.orderRepo = orderRepo;this.paymentService = paymentService;}}
The 'this' Keyword
Use `this` only in class methods. Avoid elsewhere.
// GOOD: this in class methodclass Counter {private count = 0;increment(): void {this.count++;}}// BAD: this in object literalconst counter = {count: 0,increment() {this.count++; // fragile, breaks when passed as callback},};// GOOD: closure over variablefunction createCounter() {let count = 0;return {increment: () => count++,getCount: () => count,};}
Type Inference
When Inference is Acceptable
Always explicit in function signatures. Infer in local variables, loops, destructuring, and intermediate calculations.
// GOOD: explicit function signature, inferred localsfunction processUsers(users: ReadonlyArray<User>): Array<ProcessedUser> {const results: Array<ProcessedUser> = [];for (const user of users) { // user inferred as Userconst name = user.name; // name inferred as stringconst upper = name.toUpperCase(); // upper inferred as stringconst processed = {id: user.id, name: upper}; // processed inferredresults.push(processed);}return results;}// GOOD: destructuring with inferencefunction formatUser({name, email}: User): string {return `${name} <${email}>`;}// BAD: missing return typefunction processUsers(users: ReadonlyArray<User>) {// ...}// BAD: excessive annotations on localsfunction processUsers(users: ReadonlyArray<User>): Array<ProcessedUser> {const results: Array<ProcessedUser> = [];for (const user: User of users) {const name: string = user.name;const upper: string = name.toUpperCase();// ...}return results;}
Immutability
Readonly by Default
Mark reference type parameters as `Readonly<T>`. Use `const` for all bindings unless mutation needed.
// GOOD: readonly parametersfunction processData(data: Readonly<UserData>,config: Readonly<ProcessConfig>,): ProcessResult {// data and config cannot be mutatedreturn {success: true};}// GOOD: const bindingsfunction calculateTotal(items: ReadonlyArray<Item>): number {const taxRate = 0.08;const subtotal = items.reduce((sum, item) => sum + item.price, 0);const tax = subtotal * taxRate;return subtotal + tax;}// BAD: mutable parametersfunction processData(data: UserData, config: ProcessConfig): ProcessResult {data.processed = true; // mutationreturn {success: true};}
Arrays
ALWAYS use `Array<T>` or `ReadonlyArray<T>`. NEVER use `T[]` syntax.
// GOOD: Array<T> syntaxconst numbers: Array<number> = [1, 2, 3];const roles: Array<UserRole> = ['admin', 'editor'];function calculateAverage(numbers: ReadonlyArray<number>): number {return numbers.reduce((a, b) => a + b, 0) / numbers.length;}// BAD: T[] syntax (don't use this even if common in examples)const numbers: number[] = [1, 2, 3]; // NOconst roles: UserRole[] = ['admin']; // NOfunction calculateAverage(numbers: number[]): number { // NO// ...}
Why: Consistency with other generic syntax. Array<T> is explicit and matches ReadonlyArray<T>, Record<K, V>, Promise<T>, etc. The T[] syntax is muscle memory from other languages but inconsistent with TypeScript's generic patterns.
Prefer readonly outside local scope:
// GOOD: readonly array for function parameterfunction calculateAverage(numbers: ReadonlyArray<number>): number {return numbers.reduce((a, b) => a + b, 0) / numbers.length;}// GOOD: mutable array in local scopefunction processItems(items: ReadonlyArray<Item>): Array<ProcessedItem> {const results: Array<ProcessedItem> = [];for (const item of items) {results.push(transformItem(item));}return results;}
Deep Immutability
Use `Readonly<T>` for shallow immutability, `ReadonlyDeep<T>` from type-fest when you need immutability all the way down.
import type {ReadonlyDeep} from 'type-fest';// GOOD: shallow readonly for flat objectstype UserData = Readonly<{id: string;name: string;email: string;}>;// GOOD: deep readonly for nested structurestype AppConfig = ReadonlyDeep<{database: {host: string;port: number;credentials: {username: string;password: string;};};features: {enabled: Array<string>;};}>;function loadConfig(config: AppConfig): void {// config is deeply immutable// config.database.credentials.username = 'x'; // ERROR}
Mathematics and Currency
When to Use math.js
ALWAYS use math.js for:
- Currency calculations (money)
- Financial calculations (interest, ROI, profit margins)
- Precision-critical percentages
- Complex mathematical operations requiring high precision
NEVER use JavaScript `number` for:
- Money / currency amounts
- Financial reporting calculations
- Any calculation where precision errors are unacceptable
import { create, all, MathJsInstance } from 'mathjs';const math: MathJsInstance = create(all);// GOOD: math.js for currency calculationsfunction calculateTotal(price: number,quantity: number,taxRate: number): string {const subtotal = math.multiply(math.bignumber(price),math.bignumber(quantity));const tax = math.multiply(subtotal, math.bignumber(taxRate));const total = math.add(subtotal, tax);return math.format(total, { precision: 14 });}// GOOD: math.js for financial calculationsfunction calculateROI(initialInvestment: number,finalValue: number): string {const initial = math.bignumber(initialInvestment);const final = math.bignumber(finalValue);const difference = math.subtract(final, initial);const ratio = math.divide(difference, initial);const percentage = math.multiply(ratio, 100);return math.format(percentage, { precision: 14 });}// BAD: JavaScript number for currencyfunction calculateTotal(price: number, quantity: number, taxRate: number): number {const subtotal = price * quantity; // NO: precision errorsconst tax = subtotal * taxRate; // NO: compounding errorsreturn subtotal + tax; // NO: wrong for money}// BAD: JavaScript number for percentages in financefunction calculateDiscount(price: number, discountPercent: number): number {return price * (discountPercent / 100); // NO: precision errors}
Why math.js:
- JavaScript's native
numberuses IEEE 754 double-precision floating-point - This causes precision errors:
0.1 + 0.2 !== 0.3 - For financial calculations, these errors are unacceptable
- math.js BigNumber provides arbitrary precision arithmetic
When JavaScript number is OK:
- Counters and indices
- Simple integer math (within safe integer range)
- Display coordinates, dimensions
- Non-critical calculations where precision doesn't matter
Nullability
Null vs Undefined
Use `null` for absent values. `undefined` means uninitialized. Proactively coalesce to null.
// GOOD: null for absent, undefined for uninitializedtype User = {name: string;email: string;phone: string | null; // may be absent};function findUser(id: string): User | null {const user = database.users.get(id);return user ?? null; // coalesce undefined to null}// GOOD: optional properties use ?:type UserOptions = {name: string;email: string;newsletter?: boolean; // may be undefined};// BAD: undefined for absent valuesfunction findUser(id: string): User | undefined {// prefer null for explicit absence}// GOOD: coalescing array accessconst arr: Array<number> = [1, 2, 3];const value: number | null = arr[10] ?? null;
Enums and Unions
Prefer String Literal Unions
Avoid enums. Use string literal unions instead.
// GOOD: string literal uniontype Status = 'pending' | 'active' | 'complete' | 'failed';function processStatus(status: Status): void {switch (status) {case 'pending':// handle pendingbreak;case 'active':// handle activebreak;case 'complete':// handle completebreak;case 'failed':// handle failedbreak;}}// BAD: enumenum Status {Pending = 'pending',Active = 'active',Complete = 'complete',Failed = 'failed',}
Rationale: String literal unions are simpler, work better with discriminated unions, and don't generate runtime code.
Type Safety
Never Use 'any'
Always use `unknown` for truly unknown data. If a library forces `any`, escalate to operator for replacement.
// GOOD: unknown with type guardfunction parseJSON(json: string): unknown {return JSON.parse(json);}function processData(json: string): User {const data: unknown = parseJSON(json);if (isUser(data)) {return data;}throw new Error('Invalid user data');}function isUser(value: unknown): value is User {return (typeof value === 'object' &&value !== null &&'name' in value &&'email' in value);}// BAD: using anyfunction parseJSON(json: string): any {return JSON.parse(json);}
Type Assertions
Only for TypeScript system limitations. Always include comment explaining why.
// OK: DOM API limitationconst input = document.getElementById('email') as HTMLInputElement;// DOM API returns HTMLElement, but we know it's an input// OK: after runtime validationconst data: unknown = JSON.parse(jsonString);if (isUser(data)) {const user = data; // type guard narrows to User}// BAD: assertion without validationconst user = data as User; // no runtime check// BAD: assertion to avoid type errorconst value = (someValue as any) as TargetType;
Non-null Assertion (!)
Same rules as type assertions - sparingly, with justification.
// OK: after explicit checkconst user = users.find(u => u.id === targetId);if (user) {processUser(user); // user is non-null here, no need for !}// OK (with comment): known initialization patternclass Service {private connection!: Connection;// connection initialized in async init() called by constructorconstructor() {this.init();}private async init(): Promise<void> {this.connection = await createConnection();}}// BAD: hiding real potential nullconst value = map.get(key)!; // what if key doesn't exist?
Type Guards
Use type guards to narrow unknown types. Prefer built-in checks when possible.
// GOOD: typeof/instanceof for primitives/classesfunction processValue(value: unknown): string {if (typeof value === 'string') {return value.toUpperCase();}if (typeof value === 'number') {return value.toString();}throw new Error('Unsupported type');}// GOOD: custom type guard with 'is'function isUser(value: unknown): value is User {return (typeof value === 'object' &&value !== null &&'name' in value &&typeof (value as any).name === 'string' &&'email' in value &&typeof (value as any).email === 'string');}// GOOD: discriminated uniontype Result =| {type: 'success'; data: string}| {type: 'error'; message: string};function handleResult(result: Result): void {if (result.type === 'success') {console.log(result.data); // narrowed to success} else {console.error(result.message); // narrowed to error}}// GOOD: schema validation (TypeBox preferred)import {Type, Static} from '@sinclair/typebox';const UserSchema = Type.Object({name: Type.String(),email: Type.String(),age: Type.Number(),});type User = Static<typeof UserSchema>;function validateUser(data: unknown): data is User {return Value.Check(UserSchema, data);}
Generics
Generic Constraints
Always constrain generics when possible. Use descriptive names.
// GOOD: constrained with descriptive namefunction mapItems<TItem, TResult>(items: ReadonlyArray<TItem>,mapper: (item: TItem) => TResult,): Array<TResult> {return items.map(mapper);}// GOOD: constraint on genericfunction getProperty<TObj extends object, TKey extends keyof TObj>(obj: TObj,key: TKey,): TObj[TKey] {return obj[key];}// BAD: unconstrained, single-letter namesfunction getProperty<T, K>(obj: T, key: K): any {return (obj as any)[key];}
Avoid Over-Generalization
Don't make things generic unless multiple concrete types will use it.
// GOOD: specific types for single use casefunction formatUser(user: User): string {return `${user.name} <${user.email}>`;}// BAD: unnecessary genericfunction format<T extends {name: string; email: string}>(item: T): string {return `${item.name} <${item.email}>`;}
Utility Types
Built-in vs type-fest
Use built-in utilities when available. Use type-fest for deep operations and specialized needs.
// GOOD: built-in utilitiestype PartialUser = Partial<User>;type RequiredUser = Required<User>;type UserKeys = keyof User;type UserValues = User[keyof User];// GOOD: type-fest for deep operationsimport type {PartialDeep, RequiredDeep, ReadonlyDeep} from 'type-fest';type DeepPartialConfig = PartialDeep<AppConfig>;type DeepRequiredConfig = RequiredDeep<AppConfig>;
Object Property Access
Use `Record<K, V>` for objects with dynamic keys.
// GOOD: Record for dynamic keystype UserCache = Record<string, User>;function getUser(cache: UserCache, id: string): User | null {return cache[id] ?? null;}// BAD: index signaturetype UserCache = {[key: string]: User;};
Derived Types
Use mapped types for transformations. Create explicit types for complex derivations.
// GOOD: mapped type for simple transformationtype Nullable<T> = {[K in keyof T]: T[K] | null;};type NullableUser = Nullable<User>;// GOOD: explicit type for complex casetype UserUpdateData = {name?: string;email?: string;// exclude id and other immutable fields explicitly};// BAD: overly clever utility type usagetype UserUpdateData = Omit<Partial<User>, 'id' | 'createdAt' | 'updatedAt'>;
Module Organization
Exports
Use named exports only. No default exports.
// GOOD: named exportsexport function processUser(user: User): ProcessedUser {// implementation}export type ProcessedUser = {id: string;name: string;};// BAD: default exportexport default function processUser(user: User): ProcessedUser {// implementation}
Barrel Exports
Use index.ts to re-export from directories.
// src/users/index.tsexport * from './user-service';export * from './user-repository';export * from './types';// consumers can import from directoryimport {UserService, type User} from './users';
Import Organization
Group by source type, alphabetize within groups. Use destructuring for fewer than 3 imports.
// GOOD: organized imports// External dependenciesimport {Result, ok, err} from 'neverthrow';import type {ReadonlyDeep} from 'type-fest';// Internal modulesimport {DatabaseService} from '@/services/database';import {Logger} from '@/services/logger';// Relative importsimport {UserRepository} from './user-repository';import type {User, UserData} from './types';// GOOD: destructure for < 3 importsimport {foo, bar} from './utils';// GOOD: namespace for 3+ importsimport * as utils from './utils';utils.foo();utils.bar();utils.baz();
Note: eslint-import plugin should be configured to enforce import ordering.
FCIS Integration
Note: // pattern: comments apply only to files with runtime behavior. Type-only files, constants/enum files, barrel re-exports, tests, and generated files are exempt from classification.
Functional Core Patterns
Return Result types. Never throw exceptions. Pure functions only.
// pattern: Functional Coreimport {Result, ok, err} from 'neverthrow';type ValidationError = {field: string;message: string;};// GOOD: returns Result, pure functionfunction validateUser(data: Readonly<UserData>,): Result<ValidUser, ValidationError> {if (!data.email) {return err({field: 'email', message: 'Email required'});}if (!data.name) {return err({field: 'name', message: 'Name required'});}return ok({...data, validated: true});}// GOOD: transformation with Resultfunction transformUser(user: Readonly<User>,config: Readonly<TransformConfig>,): Result<TransformedUser, TransformError> {// pure transformation logicreturn ok(transformed);}
Imperative Shell Patterns
May throw exceptions. Orchestrate I/O. Minimal business logic.
// pattern: Imperative Shellimport {HttpException} from './exceptions';class UserController {constructor(private readonly userRepo: UserRepository,private readonly logger: Logger,) {}// GOOD: orchestrates I/O, delegates to Core, may throwasync createUser(data: UserData): Promise<User> {this.logger.info('Creating user', {email: data.email});// Delegate validation to Functional Coreconst validationResult = validateUser(data);if (validationResult.isErr()) {throw new HttpException(400, validationResult.error.message);}// I/O operationconst user = await this.userRepo.create(validationResult.value);this.logger.info('User created', {id: user.id});return user;}}
Compiler Configuration
Strictness
Full strict mode plus additional checks.
{"compilerOptions": {"strict": true,"noUncheckedIndexedAccess": true,"noPropertyAccessFromIndexSignature": true,"noImplicitAny": true,"strictNullChecks": true,"strictFunctionTypes": true,"strictBindCallApply": true,"strictPropertyInitialization": true,"noImplicitThis": true,"alwaysStrict": true}}
All strict options are mandatory. No exceptions.
Testing
Test Type Safety
Allow type assertions in tests for test data setup.
// OK in tests: type assertions for test dataconst mockUser = {id: '123',name: 'Test User',} as User;// GOOD: factory functionsfunction createTestUser(overrides?: Partial<User>): User {return {id: '123',name: 'Test User',email: 'test@example.com',...overrides,};}
Tools and Libraries
Standard Stack
- Type utilities: type-fest for deep operations and specialized utilities
- Validation: TypeBox preferred over zod (avoid decorator-based libraries)
- Result types: neverthrow for functional error handling
- Linting: eslint-import for import ordering
Library Selection
When choosing between libraries, ALWAYS prefer the one without decorators.
// AVOID: decorator-based librariesimport {IsEmail, IsString} from 'class-validator';class CreateUserDto {@IsString()name: string;@IsEmail()email: string;}// PREFER: schema-based validationimport {Type} from '@sinclair/typebox';const CreateUserSchema = Type.Object({name: Type.String(),email: Type.String({format: 'email'}),});
Documentation
JSDoc for Public APIs
Use JSDoc comments for exported functions and types.
/*** Processes user data and returns a validated user object.** @param data - Raw user data to process* @returns Result containing validated user or validation error*/export function validateUser(data: Readonly<UserData>,): Result<ValidUser, ValidationError> {// implementation}/*** Configuration options for user processing.*/export type ProcessUserOptions = {/** User's full name */readonly name: string;/** User's email address */readonly email: string;/** Whether to send welcome email (default: true) */readonly sendWelcome?: boolean;};
Abstraction Guidelines
When to Abstract
Follow rule of three. Abstract when types become complex (3+ properties/levels).
// GOOD: abstract after third repetition// First useconst user1 = {id: '1', name: 'Alice', email: 'alice@example.com'};// Second useconst user2 = {id: '2', name: 'Bob', email: 'bob@example.com'};// Third use - now abstracttype User = {id: string;name: string;email: string;};// GOOD: abstract complex inline types// Beforefunction process(data: {user: {name: string; email: string};settings: {theme: string; notifications: boolean};}): void {}// After - extract when > 3 properties or nestedtype UserInfo = {name: string;email: string;};type UserSettings = {theme: string;notifications: boolean;};type ProcessData = {user: UserInfo;settings: UserSettings;};function process(data: Readonly<ProcessData>): void {}
Sharp Edges
Runtime hazards that TypeScript doesn't catch. Know these cold.
Equality
Always use `===`. Never use `==`.
// BAD: loose equality has surprising coercion"0" == false; // true[] == ![]; // truenull == undefined; // true// GOOD: strict equality"0" === false; // false[] === ![]; // falsenull === undefined; // false
TypeScript won't save you here—both are valid syntax.
Prototype Pollution
Never merge untrusted objects into plain objects.
// DANGEROUS: merging user inputconst userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');Object.assign({}, userInput); // pollutes Object.prototype// SAFE: use Map for dynamic keys from untrusted sourcesconst safeStore = new Map<string, unknown>();safeStore.set(key, value);// SAFE: null-prototype objectconst safeObj = Object.create(null) as Record<string, unknown>;// SAFE: validate keys before mergefunction safeMerge<T extends object>(target: T, source: unknown): T {if (typeof source !== 'object' || source === null) return target;for (const key of Object.keys(source)) {if (key === '__proto__' || key === 'constructor' || key === 'prototype') {continue; // skip dangerous keys}(target as Record<string, unknown>)[key] = (source as Record<string, unknown>)[key];}return target;}
Regular Expression DoS (ReDoS)
Avoid nested quantifiers and overlapping alternatives.
// DANGEROUS: catastrophic backtrackingconst bad1 = /(a+)+$/; // nested quantifiersconst bad2 = /(a|a)+$/; // overlapping alternativesconst bad3 = /(\w+)*$/; // greedy quantifier in group with quantifier// These can freeze the event loop on crafted input like "aaaaaaaaaaaaaaaaaaaaaaaa!"// SAFER: avoid nesting, use possessive-like patternsconst safer = /a+$/; // no nestingconst safest = /^[a-z]+$/; // anchored, simple character class
When accepting user-provided regex patterns, use a timeout or run in a worker.
parseInt Radix
Always specify the radix parameter.
// BAD: radix varies by engine/inputparseInt("08"); // 0 or 8 depending on engineparseInt("0x10"); // 16 (hex prefix always recognized)// GOOD: explicit radixparseInt("08", 10); // 8parseInt("10", 16); // 16parseInt("1010", 2); // 10// BETTER: use Number() for decimalNumber("08"); // 8Number.parseInt("08", 10); // 8
Array Mutations
Know which methods mutate in place.
| Mutates | Returns new array | |
|---|---|---|
.sort() | .toSorted() (ES2023) | |
.reverse() | .toReversed() (ES2023) | |
.splice() | .toSpliced() (ES2023) | |
.push(), .pop() | .concat(), .slice() | |
.shift(), .unshift() | spread: [first, ...rest] | |
.fill() | - |
// BAD: mutates originalconst original = [3, 1, 2];const sorted = original.sort(); // original is now [1, 2, 3]// GOOD: copy first (pre-ES2023)const sorted = [...original].sort();const sorted = original.slice().sort();// GOOD: use non-mutating methods (ES2023+)const sorted = original.toSorted();const reversed = original.toReversed();
Numeric Sort
Default sort is lexicographic, not numeric.
// WRONG: sorts as strings[10, 2, 1].sort(); // [1, 10, 2]// CORRECT: numeric comparator[10, 2, 1].sort((a, b) => a - b); // [1, 2, 10]// Descending[10, 2, 1].sort((a, b) => b - a); // [10, 2, 1]
eval and Function Constructor
Never use eval() or new Function() with untrusted input.
// DANGEROUS: code injectioneval(userInput); // arbitrary code executionnew Function('return ' + userInput)(); // same risk// If you need dynamic evaluation, use a sandboxed environment or parser
JSON Precision Loss
JSON.parse loses precision for large integers and BigInt.
// PROBLEM: JavaScript numbers lose precision > 2^53JSON.parse('{"id": 9007199254740993}'); // id becomes 9007199254740992// PROBLEM: BigInt not supportedJSON.parse('{"value": 123n}'); // SyntaxError// SOLUTION: use string representation for large IDstype ApiResponse = {id: string; // "9007199254740993" - keep as string};// SOLUTION: use a BigInt-aware parser for financial data// Or use string fields and parse with BigInt() after
Promise.all vs Promise.allSettled
Promise.all fails fast; Promise.allSettled waits for all.
// Promise.all: rejects immediately on first failure// Use when: all must succeed, fail fast is desiredasync function fetchAllRequired(ids: ReadonlyArray<string>): Promise<Array<User>> {const promises = ids.map(id => fetchUser(id));return Promise.all(promises); // throws on first failure}// Promise.allSettled: waits for all, never rejects// Use when: need results from successful ones even if some failasync function fetchAllBestEffort(ids: ReadonlyArray<string>,): Promise<Array<User>> {const promises = ids.map(id => fetchUser(id));const results = await Promise.allSettled(promises);return results.filter((r): r is PromiseFulfilledResult<User> => r.status === 'fulfilled').map(r => r.value);}// Common patterns with allSettledconst results = await Promise.allSettled(promises);const succeeded = results.filter(r => r.status === 'fulfilled');const failed = results.filter(r => r.status === 'rejected');// Log failures, return successesfor (const failure of failed) {if (failure.status === 'rejected') {logger.error('Operation failed', {reason: failure.reason});}}
| Method | Behavior | Use when | |
|---|---|---|---|
Promise.all | Rejects on first failure | All must succeed | |
Promise.allSettled | Always resolves with status array | Need partial results | |
Promise.race | Resolves/rejects with first to complete | Timeout patterns | |
Promise.any | Resolves with first success, rejects if all fail | First success wins |
Unsafe Property Access
Bracket notation with user input is dangerous.
// DANGEROUS: arbitrary property accessfunction getValue(obj: object, key: string): unknown {return (obj as Record<string, unknown>)[key]; // could access __proto__, constructor}// SAFER: validate or use Mapfunction safeGetValue(obj: Record<string, unknown>, key: string): unknown {if (!Object.hasOwn(obj, key)) return undefined;if (key === '__proto__' || key === 'constructor') return undefined;return obj[key];}
Common Mistakes
| Mistake | Fix | |
|---|---|---|
Using interface for data shapes | Use type instead | |
Using any in business logic | Use unknown + type guards | |
const foo = () => {} top-level declarations | Use function foo() {} | |
| Type assertions without validation | Add runtime validation or type guard | |
| Mutable parameters | Mark as Readonly<T> for reference types | |
undefined for absent values | Use null; coalesce with ?? null | |
| Enums | Use string literal unions | |
| Missing return types on exports | Always type function returns | |
Using T[] for arrays | Use Array<T> or ReadonlyArray<T> | |
JavaScript number for money/currency | Use math.js with BigNumber | |
| Decorators (unless framework requires) | Use functions or type-based solutions | |
| Default exports | Use named exports only | |
| Over-abstraction before third use | Wait for pattern to emerge | |
| Title Case error messages | Use lowercase fragments: failed to connect: timeout | |
| Unnecessary async on pure functions | Keep sync unless I/O is involved | |
== for comparisons | Use === always | |
parseInt() without radix | Use parseInt(str, 10) or Number() | |
.sort() on numeric arrays without comparator | Use .sort((a, b) => a - b) | |
Object.assign() with untrusted input | Validate keys or use Map | |
Nested regex quantifiers (a+)+ | Refactor to avoid ReDoS | |
Promise.all when partial results acceptable | Use Promise.allSettled |
Red Flags
STOP and refactor when you see:
anykeyword in business logicinterfacefor data shapes (not class contracts)- JavaScript
numberfor money, currency, or financial calculations T[]instead ofArray<T>syntax- Decorators in library selection
- Type assertions without explanatory comments
- Missing return types on exported functions
- Mutable class fields (should be
readonly) undefinedused for explicitly absent values- Enums instead of string literal unions
- Default exports
- Functions with 4+ positional parameters
- Complex inline types used repeatedly
- Async functions that don't perform I/O
- Error messages in Title Case
==instead of===eval()ornew Function()with any dynamic input- Regex patterns with nested quantifiers
(x+)+or(x|x)+ Object.assign()or spread with user-controlled objectsparseInt()without explicit radix.sort()on numbers without comparator functionJSON.parse()on data with large integer IDs (use string IDs)
Reference
For comprehensive type-fest utilities documentation, see type-fest.md.
For comprehensive TypeBox validator documentation, see typebox.md. Please note that we generally use AJV as the canonical validator, but TypeBox is the schema generator.