| 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:
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.
type UserData = {
readonly id: string;
name: string;
email: string | null;
};
interface IUserRepository {
findById(id: string): Promise<User | null>;
}
class UserRepository implements IUserRepository {
}
interface 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.
const isActive = true;
const hasPermission = checkPermission();
const canEdit = user.role === 'admin';
const shouldRetry = attempts < MAX_RETRIES;
const willTimeout = elapsed > threshold;
type User = {
active: boolean;
visible: boolean;
disabled: boolean;
};
const isDisabled = false;
const notReady = true;
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;
};
function processUser(options: ProcessUserOptions): void {
const {name, email, age, sendWelcome = true} = options;
}
function processUser({name, email, age}: {name: string, email: string, age: number}) {
}
function processUser(name: string, email: string, age: number, sendWelcome?: boolean) {
}
FooResult - Discriminated Unions
Always use discriminated unions for Result types. Integrate with neverthrow.
type ValidationResult =
| { success: true; data: ValidUser }
| { success: false; error: ValidationError };
import {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});
}
const result = validateUser(userData);
if (result.isOk()) {
console.log(result.value);
} else {
console.error(result.error);
}
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.
function processUser(data: Readonly<UserData>): ProcessResult {
return {success: true, user: data};
}
const users = rawData.map(u => transformUser(u));
button.addEventListener('click', (e) => handleClick(e));
fetch(url).then(data => processData(data));
const 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.
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
};
useEffect(() => {
}, [handleSubmit]);
const handleComplexClick = (event: MouseEvent) => {
};
element.addEventListener('click', handleComplexClick);
const calculateTotal = (items: Array<Item>): number => {
return items.reduce((sum, item) => sum + item.price, 0);
};
function 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.
type 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;
}
type SendEmailOptions = {
readonly to: string;
readonly subject: string;
readonly body?: string;
};
function sendEmail(options: SendEmailOptions): void {
}
function divide(numerator: number, denominator: number): number {
return numerator / denominator;
}
Async Functions
Always explicitly type Promise returns. Avoid async void.
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
async function saveUser(user: User): Promise<void> {
await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(user),
});
}
async function fetchUser(id: string) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
Prefer async/await over .then() chains.
async function processUserData(id: string): Promise<ProcessedUser> {
const user = await fetchUser(id);
const enriched = await enrichUserData(user);
return transformUser(enriched);
}
function 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
function transformUser(user: User): TransformedUser {
return {
fullName: `${user.firstName} ${user.lastName}`,
email: user.email.toLowerCase(),
};
}
async function loadAndTransformUser(id: string): Promise<TransformedUser> {
const user = await fetchUser(id);
return transformUser(user);
}
async 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.
class 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;
}
}
class MathUtils {
add(a: number, b: number): number {
return a + b;
}
}
function add(a: number, b: number): number {
return a + b;
}
Class Structure
Use constructor injection into private readonly fields.
class 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);
}
}
class 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.
class Counter {
private count = 0;
increment(): void {
this.count++;
}
}
const counter = {
count: 0,
increment() {
this.count++;
},
};
function 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.
function processUsers(users: ReadonlyArray<User>): Array<ProcessedUser> {
const results: Array<ProcessedUser> = [];
for (const user of users) {
const name = user.name;
const upper = name.toUpperCase();
const processed = {id: user.id, name: upper};
results.push(processed);
}
return results;
}
function formatUser({name, email}: User): string {
return `${name} <${email}>`;
}
function processUsers(users: ReadonlyArray<User>) {
}
function 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.
function processData(
data: Readonly<UserData>,
config: Readonly<ProcessConfig>,
): ProcessResult {
return {success: true};
}
function 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;
}
function processData(data: UserData, config: ProcessConfig): ProcessResult {
data.processed = true;
return {success: true};
}
Arrays
ALWAYS use Array<T> or ReadonlyArray<T>. NEVER use T[] syntax.
const 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;
}
const numbers: number[] = [1, 2, 3];
const roles: UserRole[] = ['admin'];
function calculateAverage(numbers: number[]): number {
}
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:
function calculateAverage(numbers: ReadonlyArray<number>): number {
return numbers.reduce((a, b) => a + b, 0) / numbers.length;
}
function 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';
type UserData = Readonly<{
id: string;
name: string;
email: string;
}>;
type AppConfig = ReadonlyDeep<{
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
features: {
enabled: Array<string>;
};
}>;
function loadConfig(config: AppConfig): void {
}
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);
function 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 });
}
function 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 });
}
function calculateTotal(price: number, quantity: number, taxRate: number): number {
const subtotal = price * quantity;
const tax = subtotal * taxRate;
return subtotal + tax;
}
function calculateDiscount(price: number, discountPercent: number): number {
return price * (discountPercent / 100);
}
Why math.js:
- JavaScript's native
number uses 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.
type User = {
name: string;
email: string;
phone: string | null;
};
function findUser(id: string): User | null {
const user = database.users.get(id);
return user ?? null;
}
type UserOptions = {
name: string;
email: string;
newsletter?: boolean;
};
function findUser(id: string): User | undefined {
}
const 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.
type Status = 'pending' | 'active' | 'complete' | 'failed';
function processStatus(status: Status): void {
switch (status) {
case 'pending':
break;
case 'active':
break;
case 'complete':
break;
case 'failed':
break;
}
}
enum 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.
function 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
);
}
function parseJSON(json: string): any {
return JSON.parse(json);
}
Type Assertions
Only for TypeScript system limitations. Always include comment explaining why.
const input = document.getElementById('email') as HTMLInputElement;
const data: unknown = JSON.parse(jsonString);
if (isUser(data)) {
const user = data;
}
const user = data as User;
const value = (someValue as any) as TargetType;
Non-null Assertion (!)
Same rules as type assertions - sparingly, with justification.
const user = users.find(u => u.id === targetId);
if (user) {
processUser(user);
}
class Service {
private connection!: Connection;
constructor() {
this.init();
}
private async init(): Promise<void> {
this.connection = await createConnection();
}
}
const value = map.get(key)!;
Type Guards
Use type guards to narrow unknown types. Prefer built-in checks when possible.
function processValue(value: unknown): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
if (typeof value === 'number') {
return value.toString();
}
throw new Error('Unsupported type');
}
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'
);
}
type Result =
| {type: 'success'; data: string}
| {type: 'error'; message: string};
function handleResult(result: Result): void {
if (result.type === 'success') {
console.log(result.data);
} else {
console.error(result.message);
}
}
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.
function mapItems<TItem, TResult>(
items: ReadonlyArray<TItem>,
mapper: (item: TItem) => TResult,
): Array<TResult> {
return items.map(mapper);
}
function getProperty<TObj extends object, TKey extends keyof TObj>(
obj: TObj,
key: TKey,
): TObj[TKey] {
return obj[key];
}
function 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.
function formatUser(user: User): string {
return `${user.name} <${user.email}>`;
}
function 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.
type PartialUser = Partial<User>;
type RequiredUser = Required<User>;
type UserKeys = keyof User;
type UserValues = User[keyof User];
import 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.
type UserCache = Record<string, User>;
function getUser(cache: UserCache, id: string): User | null {
return cache[id] ?? null;
}
type UserCache = {
[key: string]: User;
};
Derived Types
Use mapped types for transformations. Create explicit types for complex derivations.
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type NullableUser = Nullable<User>;
type UserUpdateData = {
name?: string;
email?: string;
};
type UserUpdateData = Omit<Partial<User>, 'id' | 'createdAt' | 'updatedAt'>;
Module Organization
Exports
Use named exports only. No default exports.
export function processUser(user: User): ProcessedUser {
}
export type ProcessedUser = {
id: string;
name: string;
};
export default function processUser(user: User): ProcessedUser {
}
Barrel Exports
Use index.ts to re-export from directories.
export * from './user-service';
export * from './user-repository';
export * from './types';
import {UserService, type User} from './users';
Import Organization
Group by source type, alphabetize within groups. Use destructuring for fewer than 3 imports.
import {Result, ok, err} from 'neverthrow';
import type {ReadonlyDeep} from 'type-fest';
import {DatabaseService} from '@/services/database';
import {Logger} from '@/services/logger';
import {UserRepository} from './user-repository';
import type {User, UserData} from './types';
import {foo, bar} from './utils';
import * 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.
import {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 required'});
}
if (!data.name) {
return err({field: 'name', message: 'Name required'});
}
return ok({...data, validated: true});
}
function transformUser(
user: Readonly<User>,
config: Readonly<TransformConfig>,
): Result<TransformedUser, TransformError> {
return ok(transformed);
}
Imperative Shell Patterns
May throw exceptions. Orchestrate I/O. Minimal business logic.
import {HttpException} from './exceptions';
class UserController {
constructor(
private readonly userRepo: UserRepository,
private readonly logger: Logger,
) {}
async createUser(data: UserData): Promise<User> {
this.logger.info('Creating user', {email: data.email});
const validationResult = validateUser(data);
if (validationResult.isErr()) {
throw new HttpException(400, validationResult.error.message);
}
const 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.
const mockUser = {
id: '123',
name: 'Test User',
} as User;
function 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.
import {IsEmail, IsString} from 'class-validator';
class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
}
import {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.
export function validateUser(
data: Readonly<UserData>,
): Result<ValidUser, ValidationError> {
}
export type ProcessUserOptions = {
readonly name: string;
readonly email: string;
readonly sendWelcome?: boolean;
};
Abstraction Guidelines
When to Abstract
Follow rule of three. Abstract when types become complex (3+ properties/levels).
const user1 = {id: '1', name: 'Alice', email: 'alice@example.com'};
const user2 = {id: '2', name: 'Bob', email: 'bob@example.com'};
type User = {
id: string;
name: string;
email: string;
};
function process(data: {
user: {name: string; email: string};
settings: {theme: string; notifications: boolean};
}): void {}
type 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 ==.
"0" == false;
[] == ![];
null == undefined;
"0" === false;
[] === ![];
null === undefined;
TypeScript won't save you here—both are valid syntax.
Prototype Pollution
Never merge untrusted objects into plain objects.
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign({}, userInput);
const safeStore = new Map<string, unknown>();
safeStore.set(key, value);
const safeObj = Object.create(null) as Record<string, unknown>;
function 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;
}
(target as Record<string, unknown>)[key] = (source as Record<string, unknown>)[key];
}
return target;
}
Regular Expression DoS (ReDoS)
Avoid nested quantifiers and overlapping alternatives.
const bad1 = /(a+)+$/;
const bad2 = /(a|a)+$/;
const bad3 = /(\w+)*$/;
const safer = /a+$/;
const safest = /^[a-z]+$/;
When accepting user-provided regex patterns, use a timeout or run in a worker.
parseInt Radix
Always specify the radix parameter.
parseInt("08");
parseInt("0x10");
parseInt("08", 10);
parseInt("10", 16);
parseInt("1010", 2);
Number("08");
Number.parseInt("08", 10);
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() | - |
const original = [3, 1, 2];
const sorted = original.sort();
const sorted = [...original].sort();
const sorted = original.slice().sort();
const sorted = original.toSorted();
const reversed = original.toReversed();
Numeric Sort
Default sort is lexicographic, not numeric.
[10, 2, 1].sort();
[10, 2, 1].sort((a, b) => a - b);
[10, 2, 1].sort((a, b) => b - a);
eval and Function Constructor
Never use eval() or new Function() with untrusted input.
eval(userInput);
new Function('return ' + userInput)();
JSON Precision Loss
JSON.parse loses precision for large integers and BigInt.
JSON.parse('{"id": 9007199254740993}');
JSON.parse('{"value": 123n}');
type ApiResponse = {
id: string;
};
Promise.all vs Promise.allSettled
Promise.all fails fast; Promise.allSettled waits for all.
async function fetchAllRequired(ids: ReadonlyArray<string>): Promise<Array<User>> {
const promises = ids.map(id => fetchUser(id));
return Promise.all(promises);
}
async 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);
}
const results = await Promise.allSettled(promises);
const succeeded = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
for (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.
function getValue(obj: object, key: string): unknown {
return (obj as Record<string, unknown>)[key];
}
function 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:
any keyword in business logic
interface for data shapes (not class contracts)
- JavaScript
number for money, currency, or financial calculations
T[] instead of Array<T> syntax
- Decorators in library selection
- Type assertions without explanatory comments
- Missing return types on exported functions
- Mutable class fields (should be
readonly)
undefined used 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() or new Function() with any dynamic input
- Regex patterns with nested quantifiers
(x+)+ or (x|x)+
Object.assign() or spread with user-controlled objects
parseInt() without explicit radix
.sort() on numbers without comparator function
JSON.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.