
Accelint Ts Best Practices
- 280 installs
- 21 repo stars
- Updated August 4, 2026
- gohypergiant/agent-skills
For development and infrastructure management.
About
accelint-ts-best-practices is an AI coding tool that enhances development workflows. Builders use it for infrastructure, integration, and platform development within the catalog ecosystem.
- accelint-ts-best-practices
- Development
Accelint Ts Best Practices by the numbers
- 280 all-time installs (skills.sh)
- Ranked #1,391 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gohypergiant/agent-skills --skill accelint-ts-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 280 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 4, 2026 |
| Repository | gohypergiant/agent-skills ↗ |
What it does
For development and infrastructure management.
Files
JavaScript and TypeScript Best Practices
Comprehensive coding standards for JavaScript and TypeScript applications, designed for AI agents and LLMs working with modern JavaScript/TypeScript codebases.
Note: This skill focuses on general best practices, TypeScript patterns, and safety. For performance optimization, use the accelint-ts-performance skill instead.
When to Use This Skill
This skill provides expert-level patterns for JavaScript and TypeScript code. Load AGENTS.md to scan rule summaries and identify relevant optimizations for your task.
How to Use
This skill uses a progressive disclosure structure to minimize context usage:
1. Start with the Overview (AGENTS.md)
Read AGENTS.md for a concise overview of all rules with one-line summaries organized by category.
2. Load Specific Rules as Needed
When you identify a relevant pattern or issue, load the corresponding reference file for detailed implementation guidance:
Quick Start:
- quick-start.md - Complete workflow examples with before/after code
General Best Practices:
- naming-conventions.md - Descriptive names, qualifier ordering, boolean prefixes
- functions.md - Function size, parameters, explicit values
- control-flow.md - Early returns, flat structure, block style
- state-management.md - const vs let, immutability, pure functions
- return-values.md - Return zero values instead of null/undefined
- misc.md - Line endings, defensive programming, technical debt
- code-duplication.md - Extract common patterns, DRY principle, when to consolidate
TypeScript:
- any.md - Avoid any, use unknown or generics
- enums.md - Use as const objects instead of enum
- type-vs-interface.md - Prefer type over interface
- bundler-paths.md - Use statically analyzable import and file-system paths for optimal bundling
Safety:
- input-validation.md - Validate external data with schemas
- assertions.md - Split assertions, include values
- error-handling.md - Handle all errors explicitly
- error-messages.md - User-friendly vs developer-specific messages
Performance:
- For performance optimization tasks, use the
accelint-ts-performanceskill for comprehensive profiling workflows and optimization patterns
Documentation:
- For documentation tasks, use the
accelint-ts-documentationskill for comprehensive JSDoc and comment guidance
3. Apply the Pattern
Each reference file contains:
- ❌ Incorrect examples showing the anti-pattern
- ✅ Correct examples showing the optimal implementation
- Explanations of why the pattern matters
4. Use the Report Template
When this skill is invoked, use the standardized report format:
Template: `assets/output-report-template.md`
The report format provides:
- Executive Summary with impact assessment
- Severity levels (Critical, High, Medium, Low) for prioritization
- Impact analysis (potential bugs, type safety, maintainability, runtime failures)
- Categorization (Type Safety, Safety, State Management, Return Values, Code Quality)
- Pattern references linking to detailed guidance in references/
- Phase 2 summary table for tracking all issues
When to use the audit template:
- Skill invoked directly via
/accelint-ts-best-practices <path> - User asks to "review code quality" or "audit code" across file(s), invoking skill implicitly
When NOT to use the report template:
- User asks to "fix this type error" (direct implementation)
- User asks "what's wrong with this code?" (answer the question)
- User requests specific fixes (apply fixes directly without formal report)
JavaScript and TypeScript Best Practices
Abstract
Comprehensive coding standards for JavaScript and TypeScript applications, designed for AI agents and LLMs. This guide focuses on code correctness, type safety, and defensive programming. Each rule includes one-line summaries here, with links to detailed examples in the references/ folder. Load reference files only when you need detailed implementation guidance for a specific rule.
Note: For performance optimization tasks, use the accelint-ts-performance skill instead. For documentation tasks, use the accelint-ts-documentation skill.
---
How to Use This Guide
1. Start here: Scan the rule summaries to identify relevant patterns 2. Load references as needed: Click through to detailed examples only when implementing 3. Progressive loading: Each reference file is self-contained with ❌/✅ examples
This structure minimizes context usage while providing complete implementation guidance when needed.
---
Loading Strategy
When writing new code:
- MANDATORY: Read quick-start.md for complete workflow examples
- Load specific pattern files only as you encounter relevant scenarios
- Start with the pattern, then check related safety rules
When reviewing existing code:
- Start with the rule summaries below to identify anti-patterns
- Load corresponding reference files for detailed fixes
- Do NOT load all references at once - use progressive loading
When fixing type errors:
- Read any.md if encountering
anytypes - Read enums.md if replacing
enum - Read type-vs-interface.md for declaration choices
When implementing safety features:
- MANDATORY: Read input-validation.md for external data
- Read bounded-iteration.md for any loops or queues
- Read error-handling.md for proper error patterns
General rule: Load reference files on-demand based on the specific pattern you're implementing. Each reference is self-contained with ❌/✅ examples.
---
Critical Anti-Patterns
NEVER do these - they appear in codebases frequently but significantly degrade code quality, safety, or maintainability:
- NEVER use
anytype - useunknownfor truly unknown types or generics for flexible types - NEVER use
enumkeyword - useas constobjects to avoid extra JavaScript output and runtime overhead - NEVER use
interfacefor simple type aliases - usetypeinstead; reserveinterfaceonly for declaration merging or legacy API compatibility - NEVER mutate function parameters - creates hidden side effects and breaks pure function principles
- NEVER return
nullorundefined- return zero values instead ([], {}, 0, "") to eliminate downstream null checks - NEVER create unbounded loops or queues - set explicit limits to prevent runaway resource consumption and crashes
See individual reference files for detailed alternatives and ✅ correct patterns.
---
1. General Best Practices
1.1 Naming Conventions
Append qualifiers in descending order (latencyMsMax not maxLatencyMs) to enable autocomplete grouping; prefix booleans with is/has. View detailed examples
1.2 Functions
Keep functions under 50 lines; explicitly type return values; avoid defaults; use function keyword for pure functions. View detailed examples
1.3 Control Flow
Always use block style { } for control flow (prevents bugs when adding code); use early returns for guard clauses. View detailed examples
1.4 State Management
Use const to signal immutability; never mutate function parameters (creates hidden side effects); keep leaf functions pure for testability. View detailed examples
1.5 Return Values
Return zero values ([], {}, 0, '') instead of null/undefined to eliminate defensive null checks and enable method chaining and composition. View detailed examples
1.6 Misc
Use Linux line endings; employ defensive programming; aim for zero technical debt. View detailed examples
1.7 Code Duplication (DRY Principle)
Extract common patterns into utility functions; consolidate duplicated logic; apply DRY when abstraction reduces complexity. View detailed examples
---
2. TypeScript
2.1 Any
Never use any (disables type checking and propagates through codebase); use unknown to force validation or generics to preserve types. View detailed examples
2.2 Enums
Never use enum (generates 5+ lines of runtime code per enum); use as const objects for zero-cost type-safe constants. View detailed examples
2.3 Type vs. Interface
Prefer type over interface; use interface only for declaration merging or class contracts. View detailed examples
2.4 Bundler-Friendly Paths
Use statically analyzable paths for imports and file-system operations; avoid dynamic path composition that prevents build-time analysis. View detailed examples
---
3. Safety
3.1 Input Validation
Always validate and sanitize external data at system boundaries with schemas. View detailed examples
3.2 Assertions
Use assertions to detect programmer errors; split compound assertions; include values. View detailed examples
3.3 Error Handling
Handle all errors explicitly; use Result types or try-catch blocks; never swallow errors silently. View detailed examples
3.4 Error Messages
Make user errors empathetic and actionable; make developer errors specific with values. View detailed examples
3.5 Bounded Iteration
Never create unbounded loops, queues, or recursive calls; set explicit limits to prevent runaway resource consumption and system crashes. View detailed examples
---
4. Performance
For performance optimization tasks, use the accelint-ts-performance skill instead. It provides:
- 4-phase workflow (Profile → Analyze → Optimize → Verify)
- Comprehensive optimization patterns for all performance categories
- Profiling tool guidance (Chrome DevTools, Node.js --prof)
- Bottleneck categorization and decision frameworks
---
5. Documentation
For documentation-specific tasks, use the accelint-ts-documentation skill instead. It provides comprehensive guidance for JSDoc comments, comment markers, and code comment quality.
╭────────────────────────────╮ │ accelint-ts-best-practices │ ╰────────────────────────────╯
<!-- Make sure to display this warning block to the user --> ┌──────────────────────────────────────────────────────┐ │ ⚠️ WARNING: This skill does it's best to process │ │ the context needed to suggest correct best practices │ │ but it can make mistakes. Please make sure to read │ │ the summary section of each issue to make sure it │ │ isn't a false positive. │ └──────────────────────────────────────────────────────┘
Report: [Target Name]
<!-- INSTRUCTIONS FOR COMPLETING THIS TEMPLATE:
1. Replace [Target Name] with the specific file/module being audited (e.g., "User Authentication", "Data Processing Utils")
2. EXECUTIVE SUMMARY: Provide a high-level overview
- Summarize what was audited and the scope
- Count issues by severity and category
- Include Impact Assessment explaining the potential risks and maintainability concerns
3. PHASE 1 - ISSUE GROUPING RULES:
- Group issues when they share the SAME root cause AND same fix pattern
- Example: Multiple instances of
anytype → group together - Example: Different safety violations → separate issues
- Use subsections (4-8) for grouped issues, individual numbers (1, 2, 3) for unique issues
4. PHASE 1 - EACH ISSUE/GROUP MUST INCLUDE:
- Location (file:line or file:line-range)
- Current code with ❌ marker
- Clear explanation of the issue
- Severity (Critical, High, Medium, Low)
- Category (Type Safety, Safety, State Management, Return Values, Code Quality)
- Impact (potential bugs, type safety violations, maintainability concerns, runtime failures)
- Pattern Reference (which references/*.md file)
- Recommended Fix with ✅ marker
5. SEVERITY LEVELS:
- Critical: Could cause runtime crashes, data loss, security issues, unbounded resource consumption
Examples: unbounded iteration, missing input validation, uncaught errors
- High: Type safety violations, hidden side effects, maintainability risks
Examples: using any, mutating parameters, missing error handling
- Medium: Code quality issues affecting readability and maintainability
Examples: poor naming, missing early returns, large functions
- Low: Style preferences and minor improvements
Examples: const vs let when value never changes, missing braces (if single line is clear)
6. CATEGORIES:
- Type Safety:
any,enum,interfacevstypeissues - Safety: Input validation, error handling, assertions, bounded iteration, error messages
- State Management: Mutation,
constvslet, pure functions - Return Values: Returning
null/undefinedinstead of zero values - Code Quality: Naming conventions, function size, control flow, code duplication
7. IMPACT FIELD SHOULD DESCRIBE:
- Potential bugs that could be introduced
- Type safety violations and their consequences
- Maintainability concerns for future developers
- Runtime failure scenarios
- Security or data integrity risks
8. PHASE 2: Generate summary table from Phase 1 findings
- Include all issues with their numbers
- Keep it concise - one row per issue/group
See assets/audit-report-example.md for a real-world example. -->
Executive Summary
Completed systematic audit of [file/module path] following accelint-ts-best-practices standards. Identified [N] code correctness issues across [N] severity levels. [Brief description of what this code does and why correctness matters].
Key Findings:
- [N] Critical issues (potential crashes, security risks, unbounded resource usage)
- [N] High severity issues (type safety violations, hidden side effects)
- [N] Medium severity issues (code quality and maintainability)
- [N] Low severity issues (minor improvements)
Impact Assessment: [Explain the overall risk profile and maintainability concerns. Consider:]
- What are the potential runtime failures or security risks?
- How do type safety violations affect long-term maintainability?
- What hidden side effects or state mutations exist?
- Are there defensive programming gaps that could cause crashes?
- How do these issues affect team velocity and bug rates?
---
Phase 1: Identified Issues
1. [Function/Location] - [Issue Type]
Location: [file:line] or [file:line-range]
// ❌ Current: [Brief description of problem]
[code snippet showing the issue]Issue:
- [Point 1 explaining the problem]
- [Point 2 with specifics about the violation]
- [Point 3 quantifying the impact if possible]
Severity: [Critical|High|Medium|Low] Category: [Type Safety|Safety|State Management|Return Values|Code Quality] Impact:
- Potential bugs: [What could go wrong at runtime]
- Type safety: [How types are compromised or bypassed]
- Maintainability: [How this affects future development]
- Runtime failures: [Crash scenarios, error propagation issues]
Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of solution]
[code snippet showing the fix]---
2. [Function/Location] - [Issue Type]
Location: [file:line] or [file:line-range]
// ❌ Current: [Brief description of problem]
[code snippet]Issue:
- [Explanation]
Severity: [Critical|High|Medium|Low] Category: [Category Name] Impact:
- Potential bugs: [Description]
- Type safety: [Description]
- Maintainability: [Description]
- Runtime failures: [Description]
Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of solution]
[code snippet]---
3-N. [Grouped Issues] - [Shared Issue Type] ([N] instances)
<!-- Use this format when multiple issues share the same root cause and fix pattern -->
Locations:
[file:line]- [function/context][file:line]- [function/context][file:line]- [function/context]
Example from [specific location]:
// ❌ Current: [Brief description of problem]
[representative code snippet]Issue:
- [Shared root cause explanation]
- [Why this pattern is problematic]
- [Impact across all instances]
Severity: [Critical|High|Medium|Low] Category: [Category Name] Impact:
- Potential bugs: [Description across all instances]
- Type safety: [How types are compromised]
- Maintainability: [Overall maintainability impact]
- Runtime failures: [Failure scenarios]
Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of solution]
[fixed code snippet]Same pattern applies to all [N] instances:
// [Location/function 2]
// ❌ Current
[code snippet]
// ✅ Better
[fixed snippet]
// [Location/function 3]
// ❌ Current
[code snippet]
// ✅ Better
[fixed snippet]---
Phase 2: Categorized Issues
| # | Location | Issue | Category | Severity |
|---|---|---|---|---|
| 1 | [file:line] | [Brief issue description] | [Category] | [Severity] |
| 2 | [file:line] | [Brief issue description] | [Category] | [Severity] |
| 3 | [file:line] | [Brief issue description] | [Category] | [Severity] |
| 4-N | [multiple] | [Brief issue description] | [Category] | [Severity] |
Total Issues: [N] By Severity: Critical ([N]), High ([N]), Medium ([N]), Low ([N]) By Category: [Category1] ([N]), [Category2] ([N]), [Category3] ([N])
Changelog
All notable changes to this skill will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[1.2.1] - 2026-05-18
Fixed
- Added missing numbered header to bundler-paths.md reference
- Changed from
## Prefer Statically Analyzable Pathsto# 2.4 Bundler-Friendly Pathswith subtitle - Rationale: All reference files should have numbered H1 titles matching their section number in AGENTS.md for consistency and easy cross-referencing
Version
- Bumped from 1.2.0 → 1.2.1
[1.2.0] - 2026-05-18
Added
- New reference: bundler-paths.md - Guidance on writing statically analyzable import and file-system paths
- Rationale: Build tools (Next.js, Vite, webpack, rollup, esbuild) require static analysis for optimal bundling. Dynamic path composition causes larger bundles, slower builds, worse cold starts, and increased memory usage.
- Covers both import() paths and fs path operations
- Includes ❌/✅ examples and links to official documentation
Changed
- Updated SKILL.md to reference bundler-paths.md in TypeScript section
- Updated AGENTS.md section 2.4 with bundler-paths rule summary
Fixed
- Grammar correction in bundler-paths.md line 54: "statically analyze" → "statically analyzes"
Version
- Bumped from 1.1.0 → 1.2.0
[1.1.0] - 2026-05-XX
Initial documented version with comprehensive TypeScript/JavaScript best practices.
JavaScript and TypeScript Best Practices
Comprehensive coding standards and performance optimization guide for JavaScript and TypeScript applications, designed for AI agents and LLMs working with modern JavaScript/TypeScript codebases.
Overview
This skill provides structured guidance for JavaScript and TypeScript development across five categories:
- General Best Practices: Naming, control flow, state management, functions
- TypeScript: Avoid any/enum, prefer type over interface
- Safety: Input validation, assertions, error handling
- Performance: Reduce branching/looping, memoization, caching, deferred await
- Documentation: JSDoc, comment markers, code clarity
Based on "HyperStyle", a coding philosophy that prioritizes safety, performance, and developer experience, in that order. Inspired by TigerBeetle's practices, it aims to build robust, efficient, and maintainable software through disciplined engineering.
Note: This skill focuses on JavaScript/TypeScript-specific patterns. Framework-specific optimizations (React, Vue, Angular) should use their dedicated skills.
---
Quick Start
For Agents/LLMs
1. Read [SKILL.md](SKILL.md) - Understand when to activate this skill and how to use it 2. Reference [AGENTS.md](AGENTS.md) - Browse rule summaries organized by category 3. Load specific patterns - Access detailed examples in references/ as needed 4. Apply the pattern - Each reference file contains ❌/✅ examples
See references/quick-start.md for complete workflow examples with before/after code.
For Humans
This skill is optimized for AI agents but humans may find it useful for:
- Learning JavaScript/TypeScript performance optimization
- Reviewing code for common anti-patterns
- Understanding safety-first programming principles
- Systematic code quality improvement
- Writing better documentation
---
Structure
Progressive Disclosure
- SKILL.md: Activation criteria and usage workflow
- AGENTS.md: One-line summaries with links to detailed references
- references/: 33 self-contained files with ❌/✅ examples
This structure minimizes context usage while providing complete implementation guidance when needed.
Safety-First Philosophy
Design for correctness before performance:
- Validate at boundaries (all external data with schemas)
- Assertions for programmer errors (crash on corrupted state)
- Explicit error handling (no silent failures)
- Zero values (eliminate downstream null checks)
Performance Optimization Hierarchy
Optimize slowest resources first:
network >> disk >> memory >> cpuAlways benchmark assumptions. Profile to identify real bottlenecks before optimizing.
---
Contributing
When adding new patterns:
1. Create reference file in references/ following the standard format:
- Clear title and one-line summary
- ❌ Incorrect example(s) showing the anti-pattern
- ✅ Correct example(s) showing the optimal implementation
- Explanation of why the pattern matters
2. Add to AGENTS.md with one-line summary and link 3. Update SKILL.md if adding new categories 4. Consider real-world usage - Ensure patterns solve actual problems, not hypothetical ones
---
License
Apache-2.0
---
Coding Philosophy
This skill follows these principles:
1. Safety first - Correctness before performance; avoid bugs through validation and assertions 2. Performance by design - Design for performance from the start; optimize slowest operations first (network >> disk >> memory >> cpu) 3. Defensive programming - Return zero values, assert invariants, validate boundaries, handle all errors 4. Simplicity over cleverness - Prefer readable code over premature optimization 5. Measure before optimizing - Benchmark assumptions; profile to identify real bottlenecks 6. Document non-obvious patterns - Explain "why", not "what"; preserve business logic context
2.1 Avoid any Type
Never use any type. It disables TypeScript's type checking and defeats the entire purpose of using TypeScript. Use unknown for truly unknown types or generics for flexible type-safe functions.
❌ Incorrect: `any` disables type safety
function parse(input: any): any {
return JSON.parse(input);
}
const result = parse('{"name":"Alice"}');
result.nonExistentProperty; // No error! Bug lurking.✅ Correct: `unknown` forces type validation
function parseUser(input: unknown): User {
if (typeof input !== 'string') {
throw new Error('Input must be string');
}
const parsed = JSON.parse(input);
// Validate parsed matches User schema here
return parsed as User;
}
const result = parseUser('{"name":"Alice"}');
result.nonExistentProperty; // TypeScript error!Why `any` is dangerous:
1. Disables all type checking: any opts out of TypeScript. You lose autocomplete, refactoring safety, and error detection.
2. Propagates through codebase: any is infectious. Once introduced, it spreads to every variable that touches it:
const x: any = getValue();
const y = x.foo; // y is any
const z = y.bar(); // z is any
// Entire call chain loses type safety3. Hides bugs at compile time: TypeScript won't catch typos, incorrect property access, or wrong function calls on any types.
When You Think You Need any
| Scenario | Instead Use | Why |
|---|---|---|
| Unknown JSON input | unknown | Forces validation before use |
| Flexible function arg | Generics <T> | Preserves type through function |
| Third-party lib with no types | unknown or @ts-expect-error | Explicit about unsafe boundary |
| "Too hard to type" | Record<string, unknown> | At least validates it's an object |
✅ Correct: generics preserve type information
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const nums = [1, 2, 3];
const num = first(nums); // num is inferred as number | undefined
const strs = ['a', 'b'];
const str = first(strs); // str is inferred as string | undefinedWhy generics are better: The return type tracks the input type. TypeScript knows num is a number and str is a string, enabling type-safe operations downstream.
3.2 Assertions
Assertions detect programmer errors. The only appropriate response to corrupted code is to crash.
function assert(condition: boolean, message?: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}Split compound assertions for clarity.
❌ Incorrect: compound assertion
assert(a && b);✅ Correct: split assertion
assert(a);
assert(b);Include variable values in assertion messages.
❌ Incorrect: variable value not included
assert(index < items.length, 'Index error');✅ Correct: variable value included
assert(
index < items.length,
`Index out of bounds: index=${index}, items.length=${items.length}`
);4.6 Bounded Iteration
NEVER create unbounded loops, queues, or recursive calls. Always set explicit limits to prevent runaway resource consumption.
Loop Limits
❌ Incorrect: unbounded while loop
while (true) {
if (queue.isEmpty()) break;
process(queue.pop());
}
// Risk: If queue.isEmpty() never returns true, infinite loop✅ Correct: bounded iteration with max iterations
const MAX_ITERATIONS = 10000;
let iterations = 0;
while (!queue.isEmpty() && iterations < MAX_ITERATIONS) {
process(queue.pop());
iterations++;
}
if (iterations >= MAX_ITERATIONS) {
throw new Error(`Loop exceeded ${MAX_ITERATIONS} iterations - possible infinite loop`);
}Why this matters: Production systems need fail-safes. A condition that "should always become true" may fail due to bugs, corrupted state, or edge cases. Explicit iteration limits prevent infinite loops from consuming CPU and hanging the application.
Queue Limits
❌ Incorrect: unbounded queue
const queue: Task[] = [];
function addTask(task: Task): void {
queue.push(task); // No size limit
}
// Risk: Memory exhaustion if tasks are added faster than processed✅ Correct: bounded queue with max size
const MAX_QUEUE_SIZE = 1000;
class BoundedQueue<T> {
private items: T[] = [];
constructor(private readonly maxSize: number = MAX_QUEUE_SIZE) {}
push(item: T): void {
if (this.items.length >= this.maxSize) {
throw new Error(`Queue exceeded max size ${this.maxSize}`);
}
this.items.push(item);
}
pop(): T | undefined {
return this.items.shift();
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
const queue = new BoundedQueue<Task>(1000);Why this matters: Unbounded queues can grow indefinitely if producers outpace consumers. This leads to memory exhaustion and crashes. Bounded queues fail fast with clear error messages instead of silent resource depletion.
Recursion Limits
❌ Incorrect: unbounded recursion
function traverse(node: Node): void {
if (!node) return;
process(node);
traverse(node.left);
traverse(node.right);
}
// Risk: Stack overflow on deep/cyclic structures✅ Correct: bounded recursion depth
const MAX_DEPTH = 100;
function traverse(node: Node, depth = 0): void {
if (!node) return;
if (depth >= MAX_DEPTH) {
throw new Error(`Recursion exceeded ${MAX_DEPTH} levels - possible cycle or excessive depth`);
}
process(node);
traverse(node.left, depth + 1);
traverse(node.right, depth + 1);
}Why this matters: Stack overflow crashes are hard to debug. Explicit depth limits catch cycles early and provide clear error messages. For legitimate deep structures, iterative solutions with explicit stacks are safer.
Timeout Patterns
❌ Incorrect: no timeout for long operations
async function processAll(items: Item[]): Promise<void> {
for (const item of items) {
await processItem(item); // Could run forever
}
}✅ Correct: timeout for entire operation
const TIMEOUT_MS = 30000;
async function processAll(items: Item[]): Promise<void> {
const startTime = Date.now();
for (const item of items) {
const elapsed = Date.now() - startTime;
if (elapsed > TIMEOUT_MS) {
throw new Error(`Operation exceeded ${TIMEOUT_MS}ms timeout after processing ${items.indexOf(item)} items`);
}
await processItem(item);
}
}✅ Correct alternative: per-item timeout with AbortController
const ITEM_TIMEOUT_MS = 5000;
async function processWithTimeout(item: Item): Promise<void> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ITEM_TIMEOUT_MS);
try {
await processItem(item, { signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}
async function processAll(items: Item[]): Promise<void> {
for (const item of items) {
await processWithTimeout(item);
}
}Why this matters: Hung async operations can block application flow indefinitely. Timeouts ensure operations fail fast with actionable error messages rather than appearing frozen to users.
Array Length Limits
❌ Incorrect: unbounded array growth in loop
const results: Result[] = [];
for (const item of items) {
const processed = processItem(item);
results.push(...processed); // processed could contain thousands of items
}✅ Correct: check array size before adding
const MAX_RESULTS = 10000;
const results: Result[] = [];
for (const item of items) {
const processed = processItem(item);
if (results.length + processed.length > MAX_RESULTS) {
throw new Error(`Results exceeded ${MAX_RESULTS} items - possible memory issue`);
}
results.push(...processed);
}Recommended Limits
| Operation Type | Recommended Limit | Rationale |
|---|---|---|
| Loop iterations | 10,000 - 100,000 | Prevents infinite loops while allowing large datasets |
| Queue size | 1,000 - 10,000 | Prevents memory exhaustion from unbounded growth |
| Recursion depth | 100 - 1,000 | Prevents stack overflow; typical stack supports ~10k frames |
| Operation timeout | 30s - 5min | Prevents hung operations; depends on expected duration |
| Array length | 10,000 - 1,000,000 | Depends on element size and available memory |
| String length | 1MB - 100MB | Prevents memory issues from pathological inputs |
Adjust limits based on:
- Available system resources (memory, CPU)
- Expected data sizes in production
- Performance requirements
- Error recovery strategy
When to Use Each Pattern
| Scenario | Pattern | Example |
|---|---|---|
Any while(true) or do-while loop | Loop iteration counter | Processing queue until empty |
| Task queues, event queues, buffers | Queue size limit | Job processing system |
| Tree traversal, graph algorithms | Recursion depth limit | JSON parsing, DOM traversal |
| Async operations, network calls | Timeout | API calls, file I/O |
| Arrays built in loops | Array length check | Aggregating results |
| String concatenation in loops | String length check | Building large text output |
Production Considerations
Make limits configurable:
interface BoundedLoopConfig {
maxIterations?: number;
timeout?: number;
onLimitExceeded?: (reason: string) => void;
}
async function processWithLimits(
items: Item[],
config: BoundedLoopConfig = {}
): Promise<void> {
const maxIterations = config.maxIterations ?? 10000;
const timeout = config.timeout ?? 30000;
const startTime = Date.now();
for (let i = 0; i < items.length && i < maxIterations; i++) {
if (Date.now() - startTime > timeout) {
const reason = `Timeout after ${timeout}ms`;
config.onLimitExceeded?.(reason);
throw new Error(reason);
}
await processItem(items[i]);
}
}Log when approaching limits:
const MAX_ITERATIONS = 10000;
const WARN_THRESHOLD = 0.8; // Warn at 80%
let iterations = 0;
while (condition && iterations < MAX_ITERATIONS) {
iterations++;
if (iterations === Math.floor(MAX_ITERATIONS * WARN_THRESHOLD)) {
console.warn(`Loop approaching limit: ${iterations}/${MAX_ITERATIONS} iterations`);
}
process();
}Graceful degradation:
// Instead of throwing, return partial results
function processUpToLimit(items: Item[], limit = 10000): ProcessResult {
const results: Result[] = [];
const processed = Math.min(items.length, limit);
for (let i = 0; i < processed; i++) {
results.push(processItem(items[i]));
}
return {
results,
processed,
total: items.length,
truncated: items.length > limit,
};
}2.4 Bundler-Friendly Paths
Prefer statically analyzable paths.
Build tools work best when import and file-system paths are obvious at build time. If you hide the real path inside a variable or compose it too dynamically, the tool either has to include a broad set of possible files, warn that it cannot analyze the import, or widen file tracing to stay safe.
Prefer explicit maps or literal paths so the set of reachable files stays narrow and predictable. This is the same rule whether you are choosing modules with import() or reading files in server/build code.
When analysis becomes too broad, the cost is real:
- Larger server bundles
- Slower builds
- Worse cold starts
- More memory use
Import Paths
❌ Incorrect: the bundler cannot tell what may be imported
const PAGE_MODULES = {
home: './pages/home',
settings: './pages/settings',
} as const
const Page = await import(PAGE_MODULES[pageName])✅ Correct: use an explicit map of allowed modules
const PAGE_MODULES = {
home: () => import('./pages/home'),
settings: () => import('./pages/settings'),
} as const
const Page = await PAGE_MODULES[pageName]()File-System Paths
❌ Incorrect: a 2-value enum still hides the final path from static analysis
const baseDir = path.join(process.cwd(), 'content/' + contentKind)✅ Correct: make each final path literal at the callsite
const baseDir =
kind === ContentKind.Blog
? path.join(process.cwd(), 'content/blog')
: path.join(process.cwd(), 'content/docs')In Next.js server code, this matters for output file tracing too. path.join(process.cwd(), someVar) can widen the traced file set because Next.js statically analyzes import, require, and fs usage.
Impacts: nextjs, vite, webpack, rollup, esbuild, and more.
References:
1.7 Code Duplication (DRY Principle)
When refactoring code, look for common patterns that can be extracted into utility functions. Apply the DRY (Don't Repeat Yourself) principle by identifying duplicated logic and consolidating it into reusable functions.
When to Extract Common Patterns
Extract common patterns when:
- The same logic appears in multiple places (2+ occurrences)
- Parameters differ but the core logic is identical
- The abstraction reduces complexity rather than adding it
- The utility function has a clear, single responsibility
Guidelines
- The extracted function should be more maintainable than the duplicated code
- Prefer simple, focused utility functions over complex abstractions
- Use descriptive names that clearly indicate the function's purpose
- Keep the utility function in the same file if used only there; move to a utilities module if used across multiple files
- Ensure the abstraction doesn't hurt readability (3 lines duplicated may not need extraction)
Examples
Example 1: Extracting Padding Logic
❌ Incorrect: duplicated padding logic
const TWO_DIGIT_DEFAULT = '--';
const FOUR_DIGIT_DEFAULT = '----';
export function formatCodeM1(value?: string | number): string {
if (value) {
return `${value}`.padStart(2, '0');
}
return TWO_DIGIT_DEFAULT;
}
export function formatCodeM2(value?: string | number): string {
if (value) {
return `${value}`.padStart(4, '0');
}
return FOUR_DIGIT_DEFAULT;
}✅ Correct: extracted common pattern with optimizations
const TWO_DIGIT_DEFAULT = '--';
const FOUR_DIGIT_DEFAULT = '----';
function formatCode(
value: string | number | undefined,
digits: 2 | 4,
): string {
if (!value) {
return digits === 2 ? TWO_DIGIT_DEFAULT : FOUR_DIGIT_DEFAULT;
}
// Optimize string coercion: avoid template literal overhead
const str = typeof value === 'string' ? value : String(value);
return str.padStart(digits, '0');
}
export function formatCodeM1(value?: string | number): string {
return formatCode(value, 2);
}
export function formatCodeM2(value?: string | number): string {
return formatCode(value, 4);
}Example 2: Extracting Validation Logic
❌ Incorrect: duplicated validation
export function updateEmail(email: string): void {
if (!email || email.trim() === '') {
throw new Error('Email is required');
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
throw new Error('Invalid email format');
}
// Update email logic
}
export function registerUser(email: string, password: string): void {
if (!email || email.trim() === '') {
throw new Error('Email is required');
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
throw new Error('Invalid email format');
}
// Registration logic
}✅ Correct: extracted validation function
function validateEmail(email: string): void {
if (!email || email.trim() === '') {
throw new Error('Email is required');
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
throw new Error('Invalid email format');
}
}
export function updateEmail(email: string): void {
validateEmail(email);
// Update email logic
}
export function registerUser(email: string, password: string): void {
validateEmail(email);
// Registration logic
}Example 3: Extracting Data Transformation
❌ Incorrect: duplicated transformation logic
export function getUserDisplayName(user: User): string {
if (user.firstName && user.lastName) {
return `${user.firstName} ${user.lastName}`;
}
if (user.firstName) {
return user.firstName;
}
if (user.lastName) {
return user.lastName;
}
return 'Anonymous';
}
export function getAuthorDisplayName(author: Author): string {
if (author.firstName && author.lastName) {
return `${author.firstName} ${author.lastName}`;
}
if (author.firstName) {
return author.firstName;
}
if (author.lastName) {
return author.lastName;
}
return 'Anonymous';
}✅ Correct: extracted name formatting
function formatFullName(
firstName: string | undefined,
lastName: string | undefined,
): string {
if (firstName && lastName) {
return `${firstName} ${lastName}`;
}
if (firstName) {
return firstName;
}
if (lastName) {
return lastName;
}
return 'Anonymous';
}
export function getUserDisplayName(user: User): string {
return formatFullName(user.firstName, user.lastName);
}
export function getAuthorDisplayName(author: Author): string {
return formatFullName(author.firstName, author.lastName);
}When NOT to Extract
Don't extract if:
- The duplication is incidental (similar-looking but semantically different)
- The abstraction makes the code harder to understand
- The logic is too simple (e.g., 1-2 line operations)
- Each instance is likely to diverge in future requirements
❌ Incorrect: over-abstraction
// Extracting trivial operations creates unnecessary indirection
function addOne(x: number): number {
return x + 1;
}
const result = addOne(counter); // Just use counter + 1✅ Correct: keep it simple
const result = counter + 1;Related Patterns
- See 1.2 Functions for function size and structure guidelines
- See 4.13 Currying for precomputing constant parameters
- See 1.6 Misc for zero technical debt principle
1.3 Control Flow
Block Style for Control Flow
Always use block syntax { } for control flow statements, even single-line returns. This prevents subtle bugs when adding code later and maintains visual consistency.
❌ Incorrect: inline style
if (!condition1) return /* something1 */;
if (!condition2) return /* something2 */;
if (!condition3) return /* something3 */;✅ Correct: block style
if (!condition1) {
return /* something1 */;
}
if (!condition2) {
return /* something2 */;
}
if (!condition3) {
return /* something3 */;
}
return /* something4 */;Why this matters:
1. Prevents bugs during modification: Adding a second statement to an inline conditional without adding braces silently breaks control flow:
// Dangerous inline style
if (!isValid) return;
logError(error); // Always executes! (This is the bug)
// Safe block style - bug would be obvious
if (!isValid) {
return;
logError(error); // Unreachable code warning
}2. Visual scanability: Blocks create clear vertical alignment, making the guard clause pattern immediately recognizable. Inline returns blend into the code and are easy to miss.
Early Returns for Guard Clauses
Invert conditions and return early rather than nesting. This reduces rightward drift and makes the "happy path" obvious.
❌ Incorrect: nested structure (3+ levels)
if (condition1) {
if (condition2) {
if (condition3) {
result = /* something4 */;
} else {
result = /* something3 */;
}
} else {
result = /* something2 */;
}
} else {
result = /* something1 */;
}✅ Correct: early returns (flat structure)
if (!condition1) {
return /* something1 */;
}
if (!condition2) {
return /* something2 */;
}
if (!condition3) {
return /* something3 */;
}
return /* something4 */;Why this matters: Nested conditionals hide the success path at the deepest level. Early returns make error handling peripheral and the main logic prominent. The final return statement is always the "happy path."
2.2 Avoid enum - Use as const Instead
Never use TypeScript's enum keyword. Use as const objects instead. This prevents extra JavaScript code generation and provides better type inference.
❌ Incorrect: enum generates runtime code
enum Direction {
Up = "UP",
Down = "DOWN",
}
// TypeScript compiles this to JavaScript:
var Direction;
(function (Direction) {
Direction["Up"] = "UP";
Direction["Down"] = "DOWN";
})(Direction || (Direction = {}));
// Adds ~5 lines of runtime code per enum✅ Correct: `as const` is zero-cost
const Direction = {
Up: 'UP',
Down: 'DOWN',
} as const;
type Direction = (typeof Direction)[keyof typeof Direction]; // 'UP' | 'DOWN'
// TypeScript compiles to:
const Direction = {
Up: 'UP',
Down: 'DOWN',
};
// No extra runtime codeWhy `as const` is better:
1. Zero runtime cost: as const objects are plain JavaScript objects. No generated IIFE wrapper or reverse mapping code. enum adds 5+ lines of runtime JavaScript per enum.
2. Better type inference: as const infers the narrowest literal type:
const x = Direction.Up; // Type: 'UP' (literal)
// vs enum:
enum DirectionEnum { Up = "UP" }
const y = DirectionEnum.Up; // Type: DirectionEnum.Up (enum member, wider)3. Works with tree-shaking: Dead code elimination can remove unused as const properties. enum generates a closure that bundlers can't tree-shake.
4. No reverse mapping confusion: Numeric enums create reverse mappings:
enum NumericEnum { A = 0, B = 1 }
NumericEnum[0] // "A" - unexpected reverse lookupas const objects don't have this footgun.
5. Easier to extend: You can spread as const objects:
const Base = { A: 'a', B: 'b' } as const;
const Extended = { ...Base, C: 'c' } as const;Extracting the Union Type
const Status = {
Pending: 'pending',
Active: 'active',
Complete: 'complete',
} as const;
// Extract union type from values
type Status = (typeof Status)[keyof typeof Status];
// Result: 'pending' | 'active' | 'complete'
function setStatus(status: Status) {
// Only accepts 'pending' | 'active' | 'complete'
}
setStatus(Status.Active); // ✅
setStatus('active'); // ✅
setStatus('invalid'); // ❌ TypeScript error3.3 Error Handling
Handle all errors explicitly.
3.4 Error Messages
For users make error messages clear, empathetic, and actionable.
❌ Incorrect: ambiguous and not human friendly
alert('Error 500: Internal Server Error');✅ Correct: descriptive and human friendly
alert(
'We\'re having trouble connecting to our server.\n' +
'Please check your internet connection and try again.'
);For developers make error messages specific, include values, and explain assumptions.
❌ Incorrect: ambiguous and lacking value
assert(typeof count === 'number', 'Type error');✅ Correct: specific and includes value
assert(
typeof count === 'number',
`Expected 'count' to be a number, but got type '${typeof count}'`
);1.2 Functions
- Keep functions under 50 lines
- Limit parameters; prefer simple return types
- Avoid default parameters; make all values explicit at call site
- Always explicitly type function return values in the function signature
- Use
functionkeyword for pure functions - Use arrow functions only for simple cases (< 3 instructions)
❌ Incorrect: implicit defaults
const position = getPosition();✅ Correct: explicit values
const position = getPosition(330);Why this matters:
1. Call-site clarity: Reading getPosition() gives no hint about what the default is. Reading getPosition(330) shows the exact value being used at this specific call site.
2. Prevents hidden bugs: Default parameters hide changes. If you change the default from 0 to 330, all existing calls silently change behavior. Explicit values make the change visible in diffs.
3. Easier refactoring: Finding all usages of a default value is hard ("where is 0 used?"). Finding all explicit 330 values is trivial with search.
4. Forces intentional choices: Requiring explicit values makes developers think about what value is appropriate for each call site rather than accepting a generic default.
❌ Incorrect: missing return type annotation
function getUser(id: string) {
return users.find(u => u.id === id);
}
const processData = (input: unknown) => {
return JSON.parse(input);
};✅ Correct: explicit return type annotation
function getUser(id: string): User | undefined {
return users.find(u => u.id === id);
}
const processData = (input: unknown): Record<string, unknown> => {
return JSON.parse(input);
};Why this matters:
1. Catches refactoring errors: If you change function implementation and accidentally change the return type, TypeScript catches it immediately. Without explicit return types, the type silently changes and breaks callers downstream.
2. Documents intent: Explicit return type User | undefined signals "this function might not find a user." Inferred types might be correct now, but could become any after a careless refactor.
3. Prevents type widening: TypeScript's inference can widen types unexpectedly. return {} infers {} (accepts any object) instead of your intended specific type. Explicit types prevent this footgun.
4. Better IDE support: Explicit return types enable better autocomplete and error detection at call sites. IDEs don't have to infer through the entire function body to understand what the function returns.
5. Faster compilation: TypeScript doesn't need to analyze function bodies to determine return types, speeding up type checking in large codebases
3.1 Input Validation
Always validate and sanitize external data at system boundaries.
❌ Incorrect: assumes valid
function validateAddress(userInput: any) {
return userInput;
}✅ Correct: asserts validity
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().length(5),
});
type Address = z.infer<typeof AddressSchema>;
function validateAddress(userInput: Address) {
return AddressSchema.safeParse(userInput);
}1.6 Misc
- Use Linux line endings (
\n) - Employ defensive/negative-space programming
- Return identity/zero elements instead of null/undefined
- Consider cache locality
- Aim for zero technical debt
1.1 Naming Conventions
Descending Order Rule for Qualifiers
When combining measurements with qualifiers (min, max, avg) or units (ms, px, count), append them in descending order of significance. This creates natural autocomplete grouping and prevents scattered naming.
❌ Incorrect: qualifier comes first
const maxLatencyMs = 1000;
const minLatencyMs = 100;
const avgLatencyMs = 500;
// In autocomplete these appear scattered alphabetically:
// - avgLatencyMs
// - maxLatencyMs
// - minLatencyMs✅ Correct: qualifiers appended in descending order
const latencyMsMax = 1000;
const latencyMsMin = 100;
const latencyMsAvg = 500;
// In autocomplete these group together naturally:
// - latencyMsAvg
// - latencyMsMax
// - latencyMsMinWhy this matters: Enables efficient discovery via autocomplete. Type latency and all related metrics appear together. The pattern scales:
// Cache metrics grouped by prefix
const cacheHitsCount = 0;
const cacheMissesCount = 0;
const cacheRatioPercent = 0;
// Timing metrics grouped by prefix
const renderTimeMs = 0;
const renderTimeMsMax = 0;
const renderTimeMsMin = 0;Boolean Prefixes
Prefix boolean variables and functions with is, has, should, or can to make their type obvious.
❌ Incorrect: ambiguous type
const visible = getVisibility(); // Function? Boolean? String?
const children = countChildren(); // Array? Number? Boolean?✅ Correct: unambiguous boolean
const isVisible = getVisibility();
const hasChildren = countChildren() > 0;Why this matters:
1. Prevents type confusion: Reading visible in code, you can't tell if it's a boolean, string ('visible'/'hidden'), or function without checking the definition. isVisible is unambiguous.
2. Self-documenting conditionals: if (hasChildren) clearly checks existence, while if (children) is ambiguous - are we checking array length? Truthiness? Existence of the property?
3. IDE autocomplete grouping: Typing is shows all boolean state flags together; typing has shows all existence checks; typing should shows all permission/policy flags.
4. Reduces cognitive load: Developers can pattern-match on prefixes to understand variable behavior without reading the declaration
Quick Start Examples
Overview
This guide demonstrates the workflow for applying patterns from this skill: identify the issue, reference AGENTS.md, load the appropriate reference file, and implement the solution.
Examples
Example 1: Optimizing Array Operations
❌ Incorrect: chained array methods create multiple iterations
const result = items.filter(x => x.active).map(x => x.id);Issue: Multiple passes over the same array (O(2n)) with intermediate array allocations.
✅ Correct: single-pass reduce operation
const result = items.reduce((acc, x) =>
x.active ? [...acc, x.id] : acc,
[]
);Why this is better: Single iteration (O(n)), though still creates intermediate arrays. For very large arrays, consider using a for loop with pre-allocated array or filtering into a Set.
Reference: reduce-looping.md
Example 2: Avoiding Needless Allocations
❌ Incorrect: intermediate variable creates unnecessary allocation
function randomInt(min: number, max: number): number {
const minCeil = Math.ceil(min);
const maxFloor = Math.floor(max);
const range = maxFloor - minCeil + 1;
return Math.floor(Math.random() * range + minCeil);
}Issue: The range variable is allocated on every call, creating GC pressure in hot paths.
✅ Correct: inline simple computation
function randomInt(min: number, max: number): number {
const minCeil = Math.ceil(min);
const maxFloor = Math.floor(max);
return Math.floor(Math.random() * (maxFloor - minCeil + 1) + minCeil);
}Why this is better: Eliminates allocation of intermediate variable, reducing GC pressure when called frequently.
Reference: avoid-allocations.md
Example 3: Currying for Performance
❌ Incorrect: recompute expensive operation every call
export function round(precision: number, value: number): number {
const multiplier = 10 ** precision;
return Math.round(value * multiplier) / multiplier;
}
// In hot path
for (const price of prices) {
rounded.push(round(2, price)); // Recomputes 10 ** 2 every iteration
}Issue: Expensive exponentiation (10 ** precision) is recomputed on every call even though precision is constant.
✅ Correct: curry to precompute constant parameters
export function round(precision: number): (value: number) => number;
export function round(precision: number, value: number): number;
export function round(
precision: number,
value?: number,
): number | ((value: number) => number) {
const multiplier = 10 ** precision;
if (value === undefined) {
return (v: number) => Math.round(v * multiplier) / multiplier;
}
return Math.round(value * multiplier) / multiplier;
}
// In hot path
const roundTo2 = round(2); // Compute 10 ** 2 once
for (const price of prices) {
rounded.push(roundTo2(price)); // Reuse precomputed multiplier
}Why this is better: Exponentiation computed once and captured in closure, eliminating repeated expensive operations. Supports both curried and direct-call patterns through function overloads.
Reference: currying.md
Example 4: Caching Storage API Calls
❌ Incorrect: repeated storage reads in loop
for (const item of items) {
const theme = localStorage.getItem('theme');
applyTheme(theme, item);
// 100 iterations = 100 storage reads
}Issue: localStorage.getItem() is synchronous but slow (disk I/O). Reading same key repeatedly wastes time.
✅ Correct: cache storage reads in memory
const storageCache = new Map<string, string | null>();
function getCached(key: string): string | null {
if (!storageCache.has(key)) {
storageCache.set(key, localStorage.getItem(key));
}
return storageCache.get(key)!;
}
// Use cached version
for (const item of items) {
const theme = getCached('theme');
applyTheme(theme, item);
// 100 iterations = 1 storage read
}
// Invalidate cache when storage changes
function setAndInvalidate(key: string, value: string): void {
localStorage.setItem(key, value);
storageCache.delete(key);
}Why this is better: Reduces disk I/O from O(n) to O(1). Critical for loops over large datasets.
Reference: cache-storage-api.md
Workflow Summary
1. Identify the pattern - Recognize anti-patterns (nested conditionals, chained array methods, repeated computations) 2. Check AGENTS.md - Find the relevant category and reference file link 3. Load reference file - Read detailed examples and explanations 4. Apply the pattern - Implement the ✅ correct version 5. Verify improvement - Benchmark if performance-related, test if safety-related
1.5 Return Zero Values Instead of Null/Undefined
Always return a zero value (identity element) instead of null or undefined. This eliminates defensive null checks throughout the codebase and allows method chaining without interruption.
Zero Values by Type
| Type | Zero Value | Why |
|---|---|---|
| Array | [] | Allows .map(), .filter(), .length without checks |
| Object | {} | Allows property access, spread operator without checks |
| String | '' | Allows .length, .split(), template literals without checks |
| Number | 0 | Allows arithmetic operations without checks |
| Boolean | false | Already non-nullable |
❌ Incorrect: returns null/undefined, requires downstream checks
function makeList(someVar) {
if (!someVar) return; // Returns undefined
return toList(someVar);
}
function anotherFn() {
const baseList = makeList(/*...*/);
if (!Array.isArray(baseList)) return; // Defensive check required
return baseList.map((x) => {/*...*/});
}✅ Correct: returns zero value, no checks needed
function makeList(someVar) {
if (!someVar) return []; // Returns empty array
return toList(someVar);
}
function anotherFn() {
return makeList(/*...*/).map((x) => {/*...*/}); // No check required
}Why this matters:
1. Eliminates defensive programming: Every null/undefined return creates a landmine that forces all callers to add checks. One function returning null can cascade into dozens of null checks.
2. Enables method chaining: Zero values support the same operations as non-empty values:
// Works with empty array just like full array
[].map(fn).filter(pred).reduce(reducer, init)3. Reduces bug surface: Forgetting a null check causes runtime errors. Zero values are safe by default.
4. Aligns with monadic patterns: Zero values act as identity elements in functional composition. Empty arrays behave correctly in flatMap, reduce, etc.
Real-World Impact
Before (null-based):
function getUsers() {
if (!cache.has('users')) return null;
return cache.get('users');
}
function getActiveUsers() {
const users = getUsers();
if (!users) return null;
return users.filter(u => u.active);
}
function getUserNames() {
const active = getActiveUsers();
if (!active) return null;
return active.map(u => u.name);
}
// Caller
const names = getUserNames();
if (!names) {
console.log('No names');
} else {
console.log(names.join(', '));
}After (zero value):
function getUsers() {
if (!cache.has('users')) return [];
return cache.get('users');
}
function getActiveUsers() {
return getUsers().filter(u => u.active);
}
function getUserNames() {
return getActiveUsers().map(u => u.name);
}
// Caller
console.log(getUserNames().join(', ')); // No checks neededThe zero-value version eliminates 4 null checks and makes the code linear and composable.
1.4 State Management
Prefer const Over let
Use const for all declarations unless the variable genuinely needs reassignment. Use let only when mutation provides measurable performance benefits in hot paths.
❌ Incorrect: unnecessary let with reassignment
let color = src.substring(start + 1, end - 1);
color = color.replace(/\s/g, '');✅ Correct: single assignment with const
const color = src.substring(start + 1, end - 1).replace(/\s/g, '');Why this matters: const signals immutability at the binding level. Readers know the identifier won't be reassigned, reducing cognitive load. It doesn't prevent object mutation (use Object.freeze() for that), but eliminates entire classes of bugs from variable shadowing and temporal dead zones.
❌ Incorrect: conditional with let and delayed assignment
let result;
if (validation.success) {
result = primary.data.options.map(addIndex);
} else {
result = fallback.data.options.map(addIndex);
}✅ Correct: ternary with const and immediate assignment
const config = validation.success ? primary : fallback;
const result = config.data.options.map(addIndex);Why this matters: Delayed assignment with let creates a temporal dead zone where result is undefined. This pattern also duplicates .map(addIndex). By extracting the conditional to config, we eliminate duplication and ensure result is always defined.
Never Mutate Function Parameters
Function parameters should be treated as read-only. Mutation creates hidden side effects that violate the principle of least surprise.
❌ Incorrect: mutates parameter
function addDefaults(options) {
options.timeout = options.timeout ?? 5000;
options.retries = options.retries ?? 3;
return options;
}
const config = { timeout: 1000 };
const result = addDefaults(config);
// config is now mutated! { timeout: 1000, retries: 3 }✅ Correct: returns new object
function addDefaults(options) {
return {
timeout: 5000,
retries: 3,
...options,
};
}
const config = { timeout: 1000 };
const result = addDefaults(config);
// config is unchanged: { timeout: 1000 }
// result has defaults: { timeout: 1000, retries: 3 }Why this matters: Mutation creates action-at-a-distance. The caller doesn't expect their object to change. This breaks pure function principles and makes code difficult to reason about, especially in async contexts where mutation can cause race conditions.
Keep Leaf Functions Pure
Centralize state manipulation in parent/orchestrator functions. Leaf functions (bottom of the call stack) should be pure: same inputs always produce same outputs, no side effects.
❌ Incorrect: leaf function mutates external state
let totalPrice = 0;
function calculatePrice(items) {
for (const item of items) {
totalPrice += item.price; // Side effect!
}
}✅ Correct: pure leaf function returns value
function calculatePrice(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
let totalPrice = calculatePrice(items); // Parent manages stateWhy this matters: Pure functions are:
- Testable: No mocking required, inputs → outputs
- Cacheable: Same inputs always give same output (memoization)
- Parallelizable: No shared state means no race conditions
- Debuggable: No hidden dependencies on external state
2.3 Type vs. Interface
Prefer type over interface for type aliases, unions, intersections, branded types, and functional patterns. Use interface` only when you absolutely need:
- Declaration merging (intentional extensibility)
- Class implementation contracts (
implements) - Legacy API compatibility
❌ Incorrect: interface not preferred for use case
interface AvatarProps { avatar: string; }✅ Correct: type preferred for use case
type AvatarProps = { avatar: string; }