Lint long files (and add TS skills) (#23)

This commit is contained in:
Neil 2026-03-22 12:12:54 -07:00 committed by GitHub
parent 4d65b9e69d
commit 428c2389ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 3801 additions and 0 deletions

View file

@ -0,0 +1,94 @@
---
name: typescript
description: This skill should be used when the user asks to "optimize TypeScript performance", "speed up tsc compilation", "configure tsconfig.json", "fix type errors", "improve async patterns", or encounters TS errors (TS2322, TS2339, "is not assignable to"). Also triggers on .ts, .tsx, .d.ts file work involving type definitions, module organization, or memory management. Does NOT cover TypeScript basics, framework-specific patterns, or testing.
---
# TypeScript Best Practices
Comprehensive performance optimization guide for TypeScript applications. Contains 45 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
## When to Apply
Reference these guidelines when:
- Configuring tsconfig.json for a new or existing project
- Writing complex type definitions or generics
- Optimizing async/await patterns and data fetching
- Organizing modules and managing imports
- Reviewing code for compilation or runtime performance
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Type System Performance | CRITICAL | `type-` |
| 2 | Compiler Configuration | CRITICAL | `tscfg-` |
| 3 | Async Patterns | HIGH | `async-` |
| 4 | Module Organization | HIGH | `module-` |
| 5 | Type Safety Patterns | MEDIUM-HIGH | `safety-` |
| 6 | Memory Management | MEDIUM | `mem-` |
| 7 | Runtime Optimization | LOW-MEDIUM | `runtime-` |
| 8 | Advanced Patterns | LOW | `advanced-` |
## Table of Contents
1. [Type System Performance](references/_sections.md#1-type-system-performance) — **CRITICAL**
- 1.1 [Add Explicit Return Types to Exported Functions](references/type-explicit-return-types.md) — CRITICAL (30-50% faster declaration emit)
- 1.2 [Avoid Deeply Nested Generic Types](references/type-avoid-deep-generics.md) — CRITICAL (prevents exponential instantiation cost)
- 1.3 [Avoid Large Union Types](references/type-avoid-large-unions.md) — CRITICAL (quadratic O(n²) comparison cost)
- 1.4 [Extract Conditional Types to Named Aliases](references/type-extract-conditional-types.md) — CRITICAL (enables compiler caching, prevents re-evaluation)
- 1.5 [Limit Type Recursion Depth](references/type-limit-recursion-depth.md) — HIGH (prevents exponential type expansion when applicable)
- 1.6 [Prefer Interfaces Over Type Intersections](references/type-interfaces-over-intersections.md) — CRITICAL (2-5× faster type resolution)
- 1.7 [Simplify Complex Mapped Types](references/type-simplify-mapped-types.md) — HIGH (reduces type computation by 50-80% when applicable)
2. [Compiler Configuration](references/_sections.md#2-compiler-configuration) — **CRITICAL**
- 2.1 [Configure Include and Exclude Properly](references/tscfg-exclude-properly.md) — CRITICAL (prevents scanning thousands of unnecessary files)
- 2.2 [Enable Incremental Compilation](references/tscfg-enable-incremental.md) — CRITICAL (50-90% faster rebuilds)
- 2.3 [Enable isolatedDeclarations for Parallel Declaration Emit](references/tscfg-isolated-declarations.md) — CRITICAL (enables parallel .d.ts generation without type-checker)
- 2.4 [Enable skipLibCheck for Faster Builds](references/tscfg-skip-lib-check.md) — CRITICAL (20-40% faster compilation)
- 2.5 [Enable strictFunctionTypes for Faster Variance Checks](references/tscfg-strict-function-types.md) — CRITICAL (enables optimized variance checking)
- 2.6 [Use erasableSyntaxOnly for Node.js Native TypeScript](references/tscfg-erasable-syntax-only.md) — HIGH (prevents 100% of Node.js type-stripping runtime errors)
- 2.7 [Use isolatedModules for Single-File Transpilation](references/tscfg-isolate-modules.md) — CRITICAL (80-90% faster transpilation with bundlers)
- 2.8 [Use Project References for Large Codebases](references/tscfg-project-references.md) — CRITICAL (60-80% faster incremental builds)
3. [Async Patterns](references/_sections.md#3-async-patterns) — **HIGH**
- 3.1 [Annotate Async Function Return Types](references/async-explicit-return-types.md) — HIGH (prevents runtime errors, improves inference)
- 3.2 [Avoid await Inside Loops](references/async-avoid-loop-await.md) — HIGH (N× faster for N iterations, 10 users = 10× improvement)
- 3.3 [Avoid Unnecessary async/await](references/async-avoid-unnecessary-async.md) — HIGH (eliminates trivial Promise wrappers and improves stack traces)
- 3.4 [Defer await Until Value Is Needed](references/async-defer-await.md) — HIGH (enables implicit parallelization)
- 3.5 [Use Promise.all for Independent Operations](references/async-parallel-promises.md) — HIGH (2-10× improvement in I/O-bound code)
4. [Module Organization](references/_sections.md#4-module-organization) — **HIGH**
- 4.1 [Avoid Barrel File Imports](references/module-avoid-barrel-imports.md) — HIGH (200-800ms import cost, 30-50% larger bundles)
- 4.2 [Avoid Circular Dependencies](references/module-avoid-circular-dependencies.md) — HIGH (prevents runtime undefined errors and slow compilation)
- 4.3 [Control @types Package Inclusion](references/module-control-types-inclusion.md) — HIGH (prevents type conflicts and reduces memory usage)
- 4.4 [Use Dynamic Imports for Large Modules](references/module-dynamic-imports.md) — HIGH (reduces initial bundle by 30-70%)
- 4.5 [Use Type-Only Imports for Types](references/module-use-type-imports.md) — HIGH (eliminates runtime imports for type information)
5. [Type Safety Patterns](references/_sections.md#5-type-safety-patterns) — **MEDIUM-HIGH**
- 5.1 [Enable noUncheckedIndexedAccess](references/safety-no-unchecked-indexed-access.md) — MEDIUM-HIGH (prevents 100% of unchecked index access errors at compile time)
- 5.2 [Enable strictNullChecks](references/safety-strict-null-checks.md) — MEDIUM-HIGH (prevents null/undefined runtime errors)
- 5.3 [Prefer unknown Over any](references/safety-prefer-unknown-over-any.md) — MEDIUM-HIGH (forces type narrowing, prevents runtime errors)
- 5.4 [Use Assertion Functions for Validation](references/safety-assertion-functions.md) — MEDIUM-HIGH (reduces validation boilerplate by 50-70%)
- 5.5 [Use const Assertions for Literal Types](references/safety-const-assertions.md) — MEDIUM-HIGH (preserves literal types, enables better inference)
- 5.6 [Use Exhaustive Checks for Union Types](references/safety-exhaustive-checks.md) — MEDIUM-HIGH (prevents 100% of missing case errors at compile time)
- 5.7 [Use Type Guards for Runtime Type Checking](references/safety-use-type-guards.md) — MEDIUM-HIGH (eliminates type assertions, catches errors at boundaries)
6. [Memory Management](references/_sections.md#6-memory-management) — **MEDIUM**
- 6.1 [Avoid Closure Memory Leaks](references/mem-avoid-closure-leaks.md) — MEDIUM (prevents retained references in long-lived callbacks)
- 6.2 [Avoid Global State Accumulation](references/mem-avoid-global-state.md) — MEDIUM (prevents unbounded memory growth)
- 6.3 [Clean Up Event Listeners](references/mem-cleanup-event-listeners.md) — MEDIUM (prevents unbounded memory growth)
- 6.4 [Clear Timers and Intervals](references/mem-clear-timers.md) — MEDIUM (prevents callback retention and repeated execution)
- 6.5 [Use WeakMap for Object Metadata](references/mem-use-weakmap-for-metadata.md) — MEDIUM (prevents memory leaks, enables automatic cleanup)
7. [Runtime Optimization](references/_sections.md#7-runtime-optimization) — **LOW-MEDIUM**
- 7.1 [Avoid Object Spread in Hot Loops](references/runtime-avoid-object-spread-in-loops.md) — LOW-MEDIUM (reduces object allocations by N×)
- 7.2 [Cache Property Access in Loops](references/runtime-cache-property-access.md) — LOW-MEDIUM (reduces property lookups by N× in hot paths)
- 7.3 [Prefer Native Array Methods Over Lodash](references/runtime-prefer-array-methods.md) — LOW-MEDIUM (eliminates library overhead, enables tree-shaking)
- 7.4 [Use for-of for Simple Iteration](references/runtime-use-for-of-for-iteration.md) — LOW-MEDIUM (reduces iteration boilerplate by 30-50%)
- 7.5 [Use Modern String Methods](references/runtime-use-string-methods.md) — LOW-MEDIUM (2-5× faster than regex for simple patterns)
- 7.6 [Use Set/Map for O(1) Lookups](references/runtime-use-set-for-lookups.md) — LOW-MEDIUM (O(n) to O(1) per lookup)
8. [Advanced Patterns](references/_sections.md#8-advanced-patterns) — **LOW**
- 8.1 [Use Branded Types for Type-Safe IDs](references/advanced-branded-types.md) — LOW (prevents mixing incompatible ID types)
- 8.2 [Use satisfies for Type Validation with Inference](references/advanced-satisfies-operator.md) — LOW (prevents property access errors, enables 100% autocomplete accuracy)
- 8.3 [Use Template Literal Types for String Patterns](references/advanced-template-literal-types.md) — LOW (prevents 100% of string format errors at compile time)
## References
1. [https://github.com/microsoft/TypeScript/wiki/Performance](https://github.com/microsoft/TypeScript/wiki/Performance)
2. [https://www.typescriptlang.org/docs/handbook/](https://www.typescriptlang.org/docs/handbook/)
3. [https://v8.dev/blog](https://v8.dev/blog)
4. [https://nodejs.org/en/learn/diagnostics/memory](https://nodejs.org/en/learn/diagnostics/memory)

View file

@ -0,0 +1,26 @@
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Quantified impact (e.g., "2-10× improvement", "200ms savings")
tags: prefix, technique, related-concepts
---
## Rule Title Here
Brief explanation of the rule and why it matters (1-3 sentences). Focus on performance implications.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example here
const badExample = inefficientOperation()
```
**Correct (description of what's right):**
```typescript
// Good code example here
const goodExample = efficientOperation()
```
Reference: [Link to documentation](https://example.com)

View file

@ -0,0 +1,46 @@
# Sections
This file defines all sections, their ordering, impact levels, and descriptions.
The section ID (in parentheses) is the filename prefix used to group rules.
---
## 1. Type System Performance (type)
**Impact:** CRITICAL
**Description:** Complex types, deep generics, and large unions cause quadratic compilation time. Simplifying type definitions yields the largest compile-time gains.
## 2. Compiler Configuration (tscfg)
**Impact:** CRITICAL
**Description:** Misconfigured tsconfig causes full rebuilds and unnecessary file scanning. Proper configuration reduces compile time by 50-80%.
## 3. Async Patterns (async)
**Impact:** HIGH
**Description:** Sequential awaits create runtime waterfalls. Parallelizing async operations yields 2-10× improvement in I/O-bound code.
## 4. Module Organization (module)
**Impact:** HIGH
**Description:** Barrel files and circular dependencies force excessive module loading. Direct imports reduce bundle size and improve tree-shaking.
## 5. Type Safety Patterns (safety)
**Impact:** MEDIUM-HIGH
**Description:** Type guards, narrowing, and strict mode prevent runtime errors. Proper patterns eliminate defensive runtime checks.
## 6. Memory Management (mem)
**Impact:** MEDIUM
**Description:** Object pooling, WeakMap usage, and closure hygiene reduce GC pressure and memory leaks in long-running applications.
## 7. Runtime Optimization (runtime)
**Impact:** LOW-MEDIUM
**Description:** Loop optimization, property caching, and collection choice improve hot-path performance.
## 8. Advanced Patterns (advanced)
**Impact:** LOW
**Description:** Branded types, variance annotations, and declaration merging for specialized use cases.

View file

@ -0,0 +1,83 @@
---
title: Use Branded Types for Type-Safe IDs
impact: LOW
impactDescription: prevents mixing incompatible ID types
tags: advanced, branded-types, nominal-types, type-safety, ids
---
## Use Branded Types for Type-Safe IDs
TypeScript uses structural typing, so `string` types are interchangeable even when they represent different concepts. Branded types add a unique marker to prevent mixing incompatible values.
**Incorrect (structural typing allows mixing):**
```typescript
type UserId = string
type OrderId = string
type ProductId = string
function fetchUser(id: UserId): Promise<User> { /* ... */ }
function fetchOrder(id: OrderId): Promise<Order> { /* ... */ }
const userId: UserId = 'user-123'
const orderId: OrderId = 'order-456'
// No error - all strings are interchangeable
fetchUser(orderId) // Bug: passed OrderId to UserId parameter
fetchOrder(userId) // Bug: passed UserId to OrderId parameter
```
**Correct (branded types prevent mixing):**
```typescript
type Brand<K, T> = K & { __brand: T }
type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>
type ProductId = Brand<string, 'ProductId'>
function createUserId(id: string): UserId {
return id as UserId
}
function createOrderId(id: string): OrderId {
return id as OrderId
}
function fetchUser(id: UserId): Promise<User> { /* ... */ }
function fetchOrder(id: OrderId): Promise<Order> { /* ... */ }
const userId = createUserId('user-123')
const orderId = createOrderId('order-456')
fetchUser(orderId) // Error: Argument of type 'OrderId' is not assignable to 'UserId'
fetchOrder(userId) // Error: Argument of type 'UserId' is not assignable to 'OrderId'
fetchUser(userId) // OK
```
**For numeric types:**
```typescript
type Cents = Brand<number, 'Cents'>
type Dollars = Brand<number, 'Dollars'>
function toCents(dollars: Dollars): Cents {
return (dollars * 100) as Cents
}
function formatPrice(cents: Cents): string {
return `$${(cents / 100).toFixed(2)}`
}
const price = 29.99 as Dollars
formatPrice(price) // Error: Dollars not assignable to Cents
formatPrice(toCents(price)) // OK: '$29.99'
```
**When to use branded types:**
- Entity IDs that shouldn't be mixed
- Currency/unit conversions
- Validated strings (email, URL, slug)
- Sensitive data that needs tracking
Reference: [TypeScript Playground - Nominal Typing](https://www.typescriptlang.org/play/typescript/language-extensions/nominal-typing.ts.html)

View file

@ -0,0 +1,94 @@
---
title: Use satisfies for Type Validation with Inference
impact: LOW
impactDescription: prevents property access errors, enables 100% autocomplete accuracy
tags: advanced, satisfies, inference, validation, type-checking
---
## Use satisfies for Type Validation with Inference
The `satisfies` operator validates that a value conforms to a type while preserving the narrower inferred type. This gives you both type safety and precise autocomplete.
**Incorrect (type annotation loses literal types):**
```typescript
type ColorConfig = Record<string, [number, number, number]>
const colors: ColorConfig = {
red: [255, 0, 0],
green: [0, 255, 0],
blue: [0, 0, 255],
// Can't access colors.red - it's just string keys
}
// TypeScript doesn't know 'red' is a valid key
const redValue = colors.red // Type: [number, number, number]
const pinkValue = colors.pink // No error! Type: [number, number, number]
```
**Correct (satisfies preserves literal types):**
```typescript
type ColorConfig = Record<string, [number, number, number]>
const colors = {
red: [255, 0, 0],
green: [0, 255, 0],
blue: [0, 0, 255],
} satisfies ColorConfig
// TypeScript knows exact keys
const redValue = colors.red // Type: [number, number, number]
const pinkValue = colors.pink // Error: Property 'pink' does not exist
```
**For configuration objects:**
```typescript
interface Route {
path: string
component: () => JSX.Element
auth?: boolean
}
// Without satisfies - loses literal path types
const routes: Route[] = [
{ path: '/', component: Home },
{ path: '/users', component: Users },
]
// routes[0].path is just 'string'
// With satisfies - preserves literal paths
const routes = [
{ path: '/', component: Home },
{ path: '/users', component: Users },
] satisfies Route[]
// routes[0].path is '/'
type RoutePath = typeof routes[number]['path'] // '/' | '/users'
```
**Combining with as const:**
```typescript
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
} as const satisfies {
apiUrl: string
timeout: number
retries: number
}
// Both validated AND readonly with literal types
config.apiUrl // Type: 'https://api.example.com' (not just string)
config.timeout = 3000 // Error: Cannot assign to 'timeout' (readonly)
```
**When to use satisfies vs type annotation:**
- Use `satisfies` when you want validation but need literal types
- Use type annotation (`:`) when you want the variable to be exactly that type
- Use `as const satisfies` for readonly config with validation
Reference: [TypeScript 4.9 satisfies](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator)

View file

@ -0,0 +1,105 @@
---
title: Use Template Literal Types for String Patterns
impact: LOW
impactDescription: prevents 100% of string format errors at compile time
tags: advanced, template-literals, string-types, patterns, validation
---
## Use Template Literal Types for String Patterns
Template literal types allow defining string patterns at the type level. TypeScript validates that strings match the expected format at compile time.
**Incorrect (plain string allows any value):**
```typescript
type EventHandler = {
event: string
handler: () => void
}
const handler: EventHandler = {
event: 'click', // OK
handler: () => {}
}
const badHandler: EventHandler = {
event: 'clck', // Typo - no error
handler: () => {}
}
function addEventListener(event: string, handler: () => void): void { }
addEventListener('onlcick', () => {}) // Typo compiles fine
```
**Correct (template literal type validates pattern):**
```typescript
type DOMEvent = 'click' | 'focus' | 'blur' | 'submit' | 'change'
type EventHandlerName = `on${Capitalize<DOMEvent>}`
type EventHandler = {
event: EventHandlerName
handler: () => void
}
const handler: EventHandler = {
event: 'onClick', // OK
handler: () => {}
}
const badHandler: EventHandler = {
event: 'onClck', // Error: Type '"onClck"' is not assignable to type 'EventHandlerName'
handler: () => {}
}
```
**For CSS-like patterns:**
```typescript
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw'
type CSSValue = `${number}${CSSUnit}`
function setWidth(element: HTMLElement, width: CSSValue): void {
element.style.width = width
}
setWidth(div, '100px') // OK
setWidth(div, '2.5rem') // OK
setWidth(div, '100') // Error: Type '"100"' is not assignable to type 'CSSValue'
setWidth(div, '100pixels') // Error
```
**For API route patterns:**
```typescript
type APIVersion = 'v1' | 'v2'
type Resource = 'users' | 'orders' | 'products'
type APIRoute = `/api/${APIVersion}/${Resource}`
function fetchResource(route: APIRoute): Promise<Response> {
return fetch(route)
}
fetchResource('/api/v1/users') // OK
fetchResource('/api/v2/orders') // OK
fetchResource('/api/v3/users') // Error: 'v3' not in APIVersion
fetchResource('/users') // Error: doesn't match pattern
```
**Combining with mapped types:**
```typescript
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
interface User {
name: string
age: number
}
type UserGetters = Getters<User>
// { getName: () => string; getAge: () => number }
```
Reference: [TypeScript 4.1 Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html)

View file

@ -0,0 +1,78 @@
---
title: Avoid await Inside Loops
impact: HIGH
impactDescription: N× faster for N iterations, 10 users = 10× improvement
tags: async, loops, batching, waterfalls, performance
---
## Avoid await Inside Loops
Using `await` inside a loop creates N sequential operations. Collect promises and await them together, or use `Promise.all()` with `map()` for parallel execution.
**Incorrect (N sequential requests):**
```typescript
async function enrichUsers(userIds: string[]): Promise<EnrichedUser[]> {
const enrichedUsers: EnrichedUser[] = []
for (const userId of userIds) {
const user = await fetchUser(userId) // Waits for each request
const profile = await fetchProfile(userId)
enrichedUsers.push({ ...user, profile })
}
// 10 users × 2 requests × 100ms = 2000ms
return enrichedUsers
}
```
**Correct (parallel execution):**
```typescript
async function enrichUsers(userIds: string[]): Promise<EnrichedUser[]> {
const enrichedUsers = await Promise.all(
userIds.map(async (userId) => {
const [user, profile] = await Promise.all([
fetchUser(userId),
fetchProfile(userId),
])
return { ...user, profile }
})
)
// 10 users processed in parallel = 100ms total
return enrichedUsers
}
```
**For rate-limited APIs (chunked batching):**
```typescript
async function enrichUsers(userIds: string[]): Promise<EnrichedUser[]> {
const BATCH_SIZE = 5
const results: EnrichedUser[] = []
for (let i = 0; i < userIds.length; i += BATCH_SIZE) {
const batch = userIds.slice(i, i + BATCH_SIZE)
const batchResults = await Promise.all(
batch.map(async (userId) => {
const [user, profile] = await Promise.all([
fetchUser(userId),
fetchProfile(userId),
])
return { ...user, profile }
})
)
results.push(...batchResults)
}
return results
}
```
**When sequential loop await is acceptable:**
- Each iteration depends on the previous result
- API strictly requires sequential calls
- Processing order affects correctness
Reference: [ESLint no-await-in-loop](https://eslint.org/docs/rules/no-await-in-loop)

View file

@ -0,0 +1,63 @@
---
title: Avoid Unnecessary async/await
impact: HIGH
impactDescription: eliminates trivial Promise wrappers and improves stack traces
tags: async, promises, overhead, optimization, return-await
---
## Avoid Unnecessary async/await
Remove `async` from functions that only wrap a single Promise without using `await` for control flow. However, prefer `return await` over bare `return` inside try/catch blocks — it ensures errors are caught and produces better stack traces.
**Incorrect (trivial async wrapper with no logic):**
```typescript
async function getUser(userId: string): Promise<User> {
return userRepository.findById(userId)
// async keyword creates unnecessary Promise wrapper
// No await, no try/catch — async adds nothing here
}
async function deleteUser(userId: string): Promise<void> {
return userRepository.delete(userId)
// Same pattern — async is pure overhead
}
```
**Correct (remove async when it adds nothing):**
```typescript
function getUser(userId: string): Promise<User> {
return userRepository.findById(userId)
// Direct Promise return, no wrapper
}
function deleteUser(userId: string): Promise<void> {
return userRepository.delete(userId)
}
```
**Keep async + return await in try/catch:**
```typescript
// Correct — return await ensures the error is caught
async function getUser(userId: string): Promise<User> {
try {
return await userRepository.findById(userId)
// Without await, rejected promise skips the catch block
} catch (error) {
logger.error('Failed to fetch user', { userId, error })
throw new UserNotFoundError(userId)
}
}
```
**When async IS needed:**
- Multiple sequential await statements
- Try/catch around await (use `return await` here)
- Conditional await logic
- Complex control flow with early returns
**Note:** ESLint deprecated `no-return-await` in v8.46.0 because `return await` is both safe and produces better stack traces. Use the `@typescript-eslint/return-await` rule with `in-try-catch` setting for nuanced control.
Reference: [ESLint no-return-await deprecation](https://eslint.org/docs/latest/rules/no-return-await)

View file

@ -0,0 +1,71 @@
---
title: Defer await Until Value Is Needed
impact: HIGH
impactDescription: enables implicit parallelization
tags: async, defer, promises, optimization, performance
---
## Defer await Until Value Is Needed
Start async operations immediately but defer `await` until the value is actually required. This allows independent work to proceed while promises resolve in the background.
**Incorrect (blocks immediately, serializes independent work):**
```typescript
async function processOrder(orderId: string): Promise<OrderResult> {
const order = await fetchOrder(orderId) // Blocks here
const config = await loadProcessingConfig() // Waits for order first, unnecessarily
// config doesn't depend on order — these could run in parallel
if (order.priority === 'express') {
return processExpress(order, config)
}
return processStandard(order, config)
}
```
**Correct (deferred await):**
```typescript
async function processOrder(orderId: string): Promise<OrderResult> {
const orderPromise = fetchOrder(orderId) // Start immediately
const config = await loadProcessingConfig() // Runs while order fetches
const order = await orderPromise // Now await when needed
if (order.priority === 'express') {
return processExpress(order, config)
}
return processStandard(order, config)
}
```
**Pattern for dependent-then-independent operations:**
```typescript
async function loadUserContent(userId: string): Promise<Content> {
// Start user fetch (needed for dependent calls)
const userPromise = fetchUser(userId)
// Start independent operations immediately
const settingsPromise = fetchGlobalSettings()
const featuresPromise = fetchFeatureFlags()
// Await user for dependent operations
const user = await userPromise
const ordersPromise = fetchOrders(user.id)
const prefsPromise = fetchPreferences(user.id)
// Await all remaining
const [settings, features, orders, prefs] = await Promise.all([
settingsPromise,
featuresPromise,
ordersPromise,
prefsPromise,
])
return { user, settings, features, orders, prefs }
}
```
Reference: [V8 Blog - Fast Async](https://v8.dev/blog/fast-async)

View file

@ -0,0 +1,76 @@
---
title: Annotate Async Function Return Types
impact: HIGH
impactDescription: prevents runtime errors, improves inference
tags: async, return-types, promises, type-safety, inference
---
## Annotate Async Function Return Types
Explicit return types on async functions catch mismatches at the function boundary rather than at call sites. They also improve IDE performance by avoiding full function body inference.
**Incorrect (inferred Promise type):**
```typescript
async function fetchUserOrders(userId: string) {
const response = await fetch(`/api/users/${userId}/orders`)
if (!response.ok) {
return null // Implicit: Promise<Order[] | null>
}
return response.json() // Implicit: Promise<any>
}
// Caller has unclear type: Promise<any>
const orders = await fetchUserOrders('123')
orders.map(o => o.id) // No type error even if orders is null
```
**Correct (explicit Promise type):**
```typescript
interface Order {
id: string
total: number
status: OrderStatus
}
async function fetchUserOrders(userId: string): Promise<Order[] | null> {
const response = await fetch(`/api/users/${userId}/orders`)
if (!response.ok) {
return null
}
return response.json() as Promise<Order[]>
}
// Caller knows the exact type
const orders = await fetchUserOrders('123')
if (orders) {
orders.map(o => o.id) // Type-safe access
}
```
**For functions that might throw:**
```typescript
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E }
async function fetchUserOrders(userId: string): Promise<Result<Order[]>> {
try {
const response = await fetch(`/api/users/${userId}/orders`)
if (!response.ok) {
return { ok: false, error: new Error(`HTTP ${response.status}`) }
}
const orders = await response.json() as Order[]
return { ok: true, value: orders }
} catch (error) {
return { ok: false, error: error as Error }
}
}
```
**Benefits:**
- Errors caught at function definition, not call sites
- Better IDE autocomplete for consumers
- Self-documenting API contracts
Reference: [TypeScript Performance Wiki - Using Type Annotations](https://github.com/microsoft/TypeScript/wiki/Performance#using-type-annotations)

View file

@ -0,0 +1,63 @@
---
title: Use Promise.all for Independent Operations
impact: HIGH
impactDescription: 2-10× improvement in I/O-bound code
tags: async, promises, parallel, waterfalls, performance
---
## Use Promise.all for Independent Operations
Sequential `await` statements create request waterfalls—each operation waits for the previous one to complete. Use `Promise.all()` to execute independent async operations concurrently.
**Incorrect (sequential execution, N round trips):**
```typescript
async function loadDashboard(userId: string): Promise<Dashboard> {
const user = await fetchUser(userId) // 200ms
const orders = await fetchOrders(userId) // 300ms
const notifications = await fetchNotifications(userId) // 150ms
// Total: 650ms (sequential)
return { user, orders, notifications }
}
```
**Correct (parallel execution, wall-clock time = max latency):**
```typescript
async function loadDashboard(userId: string): Promise<Dashboard> {
const [user, orders, notifications] = await Promise.all([
fetchUser(userId), // 200ms ─┐
fetchOrders(userId), // 300ms ─┼─ Run in parallel
fetchNotifications(userId), // 150ms ─┘
])
// Total: 300ms (max of all operations)
return { user, orders, notifications }
}
```
**For error handling with partial success:**
```typescript
async function loadDashboard(userId: string): Promise<Dashboard> {
const results = await Promise.allSettled([
fetchUser(userId),
fetchOrders(userId),
fetchNotifications(userId),
])
return {
user: results[0].status === 'fulfilled' ? results[0].value : null,
orders: results[1].status === 'fulfilled' ? results[1].value : [],
notifications: results[2].status === 'fulfilled' ? results[2].value : [],
}
}
```
**When sequential is correct:**
- Operations have data dependencies (need result A to make request B)
- Rate limiting requires sequential requests
- Order of execution matters for side effects
Reference: [MDN Promise.all](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)

View file

@ -0,0 +1,101 @@
---
title: Avoid Closure Memory Leaks
impact: MEDIUM
impactDescription: prevents retained references in long-lived callbacks
tags: mem, closures, memory-leaks, callbacks, garbage-collection
---
## Avoid Closure Memory Leaks
Closures retain references to their outer scope variables. Long-lived callbacks can accidentally keep large objects alive, causing memory to grow unboundedly.
**Incorrect (closure retains entire scope):**
```typescript
function createDataProcessor(largeDataset: DataRecord[]): () => void {
const processedIds = new Set<string>()
return function processNext(): void {
// This closure retains reference to largeDataset
// even though it only needs processedIds
const next = largeDataset.find(r => !processedIds.has(r.id))
if (next) {
processedIds.add(next.id)
sendToServer(next)
}
}
}
// largeDataset (100MB) stays in memory as long as processNext exists
const processor = createDataProcessor(hugeDataset)
setInterval(processor, 1000) // Runs forever, 100MB never freed
```
**Correct (closure captures only what it needs):**
```typescript
function createDataProcessor(largeDataset: DataRecord[]): () => void {
// Build a queue of just the IDs and a lookup for individual records
const pendingQueue: string[] = largeDataset.map(r => r.id)
const getRecord = (id: string): DataRecord | undefined =>
largeDataset.find(r => r.id === id)
// Release the array reference — closure only captures pendingQueue and getRecord
// Caller should also release their reference to largeDataset
return function processNext(): void {
const nextId = pendingQueue.shift()
if (nextId) {
const record = getRecord(nextId)
if (record) sendToServer(record)
}
}
}
```
**Better pattern — accept an iterator to avoid holding the full dataset:**
```typescript
function createDataProcessor(records: Iterable<DataRecord>): () => void {
const iterator = records[Symbol.iterator]()
return function processNext(): void {
const { value, done } = iterator.next()
if (!done) {
sendToServer(value)
}
}
}
```
**For event handlers:**
```typescript
// Incorrect - handler retains component instance forever
class Dashboard {
private largeCache: Map<string, Data> = new Map()
initialize(): void {
window.addEventListener('resize', () => {
this.handleResize() // 'this' keeps entire Dashboard alive
})
}
}
// Correct - remove listener when done
class Dashboard {
private largeCache: Map<string, Data> = new Map()
private resizeHandler: () => void
initialize(): void {
this.resizeHandler = () => this.handleResize()
window.addEventListener('resize', this.resizeHandler)
}
destroy(): void {
window.removeEventListener('resize', this.resizeHandler)
this.largeCache.clear()
}
}
```
Reference: [Node.js Memory Diagnostics](https://nodejs.org/en/learn/diagnostics/memory)

View file

@ -0,0 +1,97 @@
---
title: Avoid Global State Accumulation
impact: MEDIUM
impactDescription: prevents unbounded memory growth
tags: mem, global-state, singletons, memory-leaks, caching
---
## Avoid Global State Accumulation
Global variables and module-level state persist for the application's lifetime. Unbounded caches or collections at module scope grow indefinitely, causing memory exhaustion.
**Incorrect (unbounded global cache):**
```typescript
// cache.ts
const userCache = new Map<string, User>() // Never cleared
export function getCachedUser(id: string): User | undefined {
return userCache.get(id)
}
export function cacheUser(user: User): void {
userCache.set(user.id, user)
// Cache grows forever, never evicts old entries
}
// After 1 million users, cache holds 1 million User objects
```
**Correct (bounded cache with eviction):**
```typescript
// cache.ts
class LRUCache<K, V> {
private cache = new Map<K, V>()
private maxSize: number
constructor(maxSize: number) {
this.maxSize = maxSize
}
get(key: K): V | undefined {
const value = this.cache.get(key)
if (value !== undefined) {
// Move to end (most recently used)
this.cache.delete(key)
this.cache.set(key, value)
}
return value
}
set(key: K, value: V): void {
if (this.cache.has(key)) {
this.cache.delete(key)
} else if (this.cache.size >= this.maxSize) {
// Evict oldest (first) entry
const oldest = this.cache.keys().next().value
this.cache.delete(oldest)
}
this.cache.set(key, value)
}
}
const userCache = new LRUCache<string, User>(1000) // Max 1000 entries
export function getCachedUser(id: string): User | undefined {
return userCache.get(id)
}
export function cacheUser(user: User): void {
userCache.set(user.id, user)
}
```
**For request-scoped state (Node.js):**
```typescript
import { AsyncLocalStorage } from 'async_hooks'
interface RequestContext {
userId: string
cache: Map<string, unknown>
}
const requestContext = new AsyncLocalStorage<RequestContext>()
export function runWithContext<T>(context: RequestContext, fn: () => T): T {
return requestContext.run(context, fn)
// Context is automatically cleaned up when request ends
}
export function getRequestCache(): Map<string, unknown> {
return requestContext.getStore()?.cache ?? new Map()
}
```
Reference: [Node.js Memory Management](https://nodejs.org/en/learn/diagnostics/memory)

View file

@ -0,0 +1,111 @@
---
title: Clean Up Event Listeners
impact: MEDIUM
impactDescription: prevents unbounded memory growth
tags: mem, event-listeners, cleanup, memory-leaks, lifecycle
---
## Clean Up Event Listeners
Event listeners hold references to their callback functions and bound objects. Failing to remove them when components unmount causes memory to grow with each mount/unmount cycle.
**Incorrect (listeners never removed):**
```typescript
class WebSocketManager {
private socket: WebSocket
connect(url: string): void {
this.socket = new WebSocket(url)
this.socket.addEventListener('message', (event) => {
this.handleMessage(event.data)
})
this.socket.addEventListener('error', (event) => {
this.handleError(event)
})
// Listeners keep 'this' alive even after disconnect
}
disconnect(): void {
this.socket.close()
// Listeners still attached, WebSocketManager can't be GC'd
}
}
```
**Correct (listeners removed on cleanup):**
```typescript
class WebSocketManager {
private socket: WebSocket
private messageHandler: (event: MessageEvent) => void
private errorHandler: (event: Event) => void
connect(url: string): void {
this.socket = new WebSocket(url)
this.messageHandler = (event) => this.handleMessage(event.data)
this.errorHandler = (event) => this.handleError(event)
this.socket.addEventListener('message', this.messageHandler)
this.socket.addEventListener('error', this.errorHandler)
}
disconnect(): void {
this.socket.removeEventListener('message', this.messageHandler)
this.socket.removeEventListener('error', this.errorHandler)
this.socket.close()
}
}
```
**Using AbortController (modern pattern):**
```typescript
class WebSocketManager {
private socket: WebSocket
private abortController: AbortController
connect(url: string): void {
this.abortController = new AbortController()
const { signal } = this.abortController
this.socket = new WebSocket(url)
this.socket.addEventListener('message', (e) => this.handleMessage(e.data), { signal })
this.socket.addEventListener('error', (e) => this.handleError(e), { signal })
// All listeners automatically removed when signal is aborted
}
disconnect(): void {
this.abortController.abort() // Removes all listeners at once
this.socket.close()
}
}
```
**React useEffect pattern:**
```typescript
function useWebSocket(url: string): Data | null {
const [data, setData] = useState<Data | null>(null)
useEffect(() => {
const socket = new WebSocket(url)
const handler = (event: MessageEvent) => setData(JSON.parse(event.data))
socket.addEventListener('message', handler)
return () => {
socket.removeEventListener('message', handler)
socket.close()
}
}, [url])
return data
}
```
Reference: [MDN AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)

View file

@ -0,0 +1,106 @@
---
title: Clear Timers and Intervals
impact: MEDIUM
impactDescription: prevents callback retention and repeated execution
tags: mem, timers, intervals, cleanup, memory-leaks
---
## Clear Timers and Intervals
`setInterval` and `setTimeout` callbacks retain references to their closure scope. Failing to clear them causes callbacks to execute indefinitely and prevents garbage collection of referenced objects.
**Incorrect (intervals never cleared):**
```typescript
class DataPoller {
private data: LargeDataset
start(): void {
setInterval(() => {
this.data = fetchLatestData()
this.updateDashboard()
}, 5000)
// No reference to interval ID, can't clear it
}
stop(): void {
// Can't stop the interval - it runs forever
// 'this' is retained, DataPoller can't be GC'd
}
}
// Each new DataPoller instance creates another interval
// Old instances can't be cleaned up
```
**Correct (intervals tracked and cleared):**
```typescript
class DataPoller {
private data: LargeDataset
private intervalId: ReturnType<typeof setInterval> | null = null
start(): void {
if (this.intervalId) return // Prevent duplicate intervals
this.intervalId = setInterval(() => {
this.data = fetchLatestData()
this.updateDashboard()
}, 5000)
}
stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
}
}
}
```
**For multiple timers:**
```typescript
class AnimationController {
private timers = new Set<ReturnType<typeof setTimeout>>()
scheduleAnimation(delay: number, callback: () => void): void {
const timerId = setTimeout(() => {
this.timers.delete(timerId)
callback()
}, delay)
this.timers.add(timerId)
}
cancelAll(): void {
for (const timerId of this.timers) {
clearTimeout(timerId)
}
this.timers.clear()
}
}
```
**React hook pattern:**
```typescript
function usePolling(callback: () => void, interval: number): void {
useEffect(() => {
const id = setInterval(callback, interval)
return () => clearInterval(id) // Cleanup on unmount
}, [callback, interval])
}
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(timer) // Clear on value change or unmount
}, [value, delay])
return debouncedValue
}
```
Reference: [MDN clearInterval](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)

View file

@ -0,0 +1,84 @@
---
title: Use WeakMap for Object Metadata
impact: MEDIUM
impactDescription: prevents memory leaks, enables automatic cleanup
tags: mem, weakmap, metadata, garbage-collection, memory-leaks
---
## Use WeakMap for Object Metadata
WeakMap allows garbage collection of keys when no other references exist. Use it for associating metadata with objects without preventing their cleanup.
**Incorrect (Map retains object references):**
```typescript
const userMetadata = new Map<User, UserMetadata>()
function trackUser(user: User): void {
userMetadata.set(user, {
lastSeen: Date.now(),
pageViews: 0
})
}
function removeUser(user: User): void {
// Even after user is "removed" from app state,
// Map still holds reference, preventing GC
userMetadata.delete(user) // Must manually clean up
}
// If delete is forgotten, user objects leak forever
```
**Correct (WeakMap allows GC):**
```typescript
const userMetadata = new WeakMap<User, UserMetadata>()
function trackUser(user: User): void {
userMetadata.set(user, {
lastSeen: Date.now(),
pageViews: 0
})
}
// No cleanup needed - when user object is GC'd,
// WeakMap entry is automatically removed
function processUsers(users: User[]): void {
for (const user of users) {
trackUser(user)
}
// When users array is cleared, all metadata is cleaned up automatically
}
```
**Common use cases:**
```typescript
// DOM element metadata
const elementState = new WeakMap<HTMLElement, ElementState>()
function attachState(element: HTMLElement): void {
elementState.set(element, { isExpanded: false })
// When element is removed from DOM and GC'd, state is cleaned up
}
// Caching computed values
const computedCache = new WeakMap<Config, ComputedConfig>()
function getComputedConfig(config: Config): ComputedConfig {
let computed = computedCache.get(config)
if (!computed) {
computed = expensiveComputation(config)
computedCache.set(config, computed)
}
return computed
}
```
**Limitations of WeakMap:**
- Keys must be objects (not primitives)
- Not iterable (no `.keys()`, `.values()`, `.entries()`)
- No `.size` property
Reference: [MDN WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)

View file

@ -0,0 +1,70 @@
---
title: Avoid Barrel File Imports
impact: HIGH
impactDescription: 200-800ms import cost, 30-50% larger bundles
tags: module, barrel-files, imports, tree-shaking, bundling
---
## Avoid Barrel File Imports
Barrel files (index.ts re-exports) defeat tree-shaking and force bundlers to load entire module graphs. Import directly from source files to enable proper dead-code elimination.
**Incorrect (imports entire module tree):**
```typescript
// utils/index.ts (barrel file)
export * from './string'
export * from './date'
export * from './validation'
export * from './crypto' // Heavy, rarely used
// consumer.ts
import { formatDate } from '@/utils'
// Loads ALL utils modules, including crypto
// Bundle includes 50KB of unused code
```
**Correct (direct imports):**
```typescript
// consumer.ts
import { formatDate } from '@/utils/date'
// Loads only the date module
// Bundle includes only what's used
```
**For icon libraries (common barrel offender):**
```typescript
// Incorrect - loads all 1500+ icons
import { Check, X } from 'lucide-react'
// Correct - loads only 2 icons
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
```
**Alternative (configure bundler optimization):**
```javascript
// next.config.js
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', '@mui/material', 'lodash']
}
}
// vite.config.ts
export default {
optimizeDeps: {
include: ['lucide-react']
}
}
```
**When barrels are acceptable:**
- Internal modules with few exports (< 10)
- Package entry points for library consumers
- When bundler is configured to optimize them
Reference: [Vercel - How we optimized package imports](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)

View file

@ -0,0 +1,92 @@
---
title: Avoid Circular Dependencies
impact: HIGH
impactDescription: prevents runtime undefined errors and slow compilation
tags: module, circular, dependencies, architecture, compilation
---
## Avoid Circular Dependencies
Circular dependencies cause undefined values at runtime (due to incomplete module initialization) and slow TypeScript compilation as the checker resolves cycles repeatedly.
**Incorrect (circular dependency):**
```typescript
// user.ts
import { Order } from './order'
export interface User {
id: string
orders: Order[]
}
export function createUser(): User { /* ... */ }
// order.ts
import { User } from './user' // Circular!
export interface Order {
id: string
user: User
}
export function createOrder(user: User): Order {
// 'createUser' might be undefined if order.ts loads first
}
```
**Correct (extract shared types):**
```typescript
// types.ts (no dependencies)
export interface User {
id: string
orders: Order[]
}
export interface Order {
id: string
user: User
}
// user.ts
import { User, Order } from './types'
export function createUser(): User { /* ... */ }
// order.ts
import { User, Order } from './types'
export function createOrder(user: User): Order { /* ... */ }
```
**Alternative (interface segregation):**
```typescript
// user-types.ts
export interface UserBase {
id: string
name: string
}
// order.ts
import { UserBase } from './user-types'
export interface Order {
id: string
user: UserBase // Only needs base interface, not full User
}
```
**Detection tools:**
```bash
# Madge - visualize circular dependencies
npx madge --circular --extensions ts ./src
# ESLint plugin
npm install eslint-plugin-import
# Rule: import/no-cycle
```
Reference: [Node.js Cycles Documentation](https://nodejs.org/api/modules.html#cycles)

View file

@ -0,0 +1,92 @@
---
title: Control @types Package Inclusion
impact: HIGH
impactDescription: prevents type conflicts and reduces memory usage
tags: module, types, tsconfig, declaration-files, performance
---
## Control @types Package Inclusion
By default, TypeScript loads all `@types/*` packages from `node_modules`. This causes conflicts between incompatible type versions and wastes memory loading unused declarations.
**Incorrect (loads all @types automatically):**
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext"
}
}
```
```bash
# All @types/* packages loaded:
# @types/node, @types/react, @types/express, @types/lodash,
# @types/jest, @types/mocha (conflict!), @types/jasmine (conflict!)
```
**Correct (explicit types inclusion):**
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"types": ["node", "react", "jest"]
}
}
```
```bash
# Only specified @types loaded
# No conflicts between test frameworks
```
**For different environments:**
```json
// tsconfig.json (base)
{
"compilerOptions": {
"types": []
}
}
// tsconfig.node.json (Node.js scripts)
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["node"]
}
}
// tsconfig.test.json (Jest tests)
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["node", "jest"]
}
}
```
**Using typeRoots for custom declarations:**
```json
{
"compilerOptions": {
"typeRoots": [
"./types", // Custom declarations first
"./node_modules/@types" // Then @types
],
"types": ["node"]
}
}
```
**Benefits:**
- Prevents type conflicts between similar packages
- Reduces memory usage during compilation
- Faster IDE responsiveness
Reference: [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance#controlling-types-inclusion)

View file

@ -0,0 +1,78 @@
---
title: Use Dynamic Imports for Large Modules
impact: HIGH
impactDescription: reduces initial bundle by 30-70%
tags: module, dynamic-import, code-splitting, lazy-loading, bundling
---
## Use Dynamic Imports for Large Modules
Dynamic `import()` creates separate chunks that load on demand. Use them for large dependencies, route-specific code, and features that aren't needed immediately.
**Incorrect (static import, always loaded):**
```typescript
import { PDFGenerator } from 'pdfkit' // 500KB
import { ExcelExporter } from 'exceljs' // 800KB
import { ChartLibrary } from 'chart.js' // 300KB
export async function exportReport(format: 'pdf' | 'excel' | 'chart') {
if (format === 'pdf') {
return new PDFGenerator().generate()
}
// All 1.6MB loaded even if user never exports
}
```
**Correct (dynamic import, loaded on demand):**
```typescript
export async function exportReport(format: 'pdf' | 'excel' | 'chart') {
if (format === 'pdf') {
const { PDFGenerator } = await import('pdfkit')
return new PDFGenerator().generate()
}
if (format === 'excel') {
const { ExcelExporter } = await import('exceljs')
return new ExcelExporter().export()
}
const { ChartLibrary } = await import('chart.js')
return new ChartLibrary().render()
}
// Only loads the module needed for the specific format
```
**With TypeScript typing:**
```typescript
async function loadPdfGenerator(): Promise<typeof import('pdfkit')> {
return import('pdfkit')
}
// Or with type-only import for the interface
import type { PDFDocument } from 'pdfkit'
async function generatePdf(): Promise<PDFDocument> {
const { default: PDFDocument } = await import('pdfkit')
return new PDFDocument()
}
```
**Framework-specific patterns:**
```typescript
// Next.js
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <ChartSkeleton />,
ssr: false // Skip server-side rendering
})
// React
const HeavyChart = React.lazy(() => import('@/components/HeavyChart'))
```
Reference: [MDN Dynamic Import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)

View file

@ -0,0 +1,80 @@
---
title: Use Type-Only Imports for Types
impact: HIGH
impactDescription: eliminates runtime imports for type information
tags: module, type-imports, tree-shaking, bundling, compilation
---
## Use Type-Only Imports for Types
Type-only imports (`import type`) are completely erased during compilation, preventing unnecessary runtime module loading. Regular imports of types can force module execution even when only the type is needed.
**Incorrect (runtime import for type-only usage):**
```typescript
// config.ts
import { DatabaseConfig } from './database' // Loads entire database module
import { Logger } from './logger' // Loads entire logger module
interface AppConfig {
db: DatabaseConfig
logger: Logger
}
// Runtime: database.js and logger.js are both loaded
// even though we only use their types
```
**Correct (type-only imports):**
```typescript
// config.ts
import type { DatabaseConfig } from './database'
import type { Logger } from './logger'
interface AppConfig {
db: DatabaseConfig
logger: Logger
}
// Runtime: no modules loaded, types are erased
```
**Mixed imports (types and values):**
```typescript
// Incorrect - unclear what's type vs value
import { User, createUser, UserRole } from './user'
// Correct - explicit separation
import { createUser } from './user'
import type { User, UserRole } from './user'
// Or inline type imports (TypeScript 4.5+)
import { createUser, type User, type UserRole } from './user'
```
**Enable enforcement:**
```json
// tsconfig.json
{
"compilerOptions": {
"verbatimModuleSyntax": true
}
}
// .eslintrc
{
"rules": {
"@typescript-eslint/consistent-type-imports": "error"
}
}
```
**Benefits:**
- Smaller bundles (unused modules not included)
- Faster cold starts (fewer modules to parse)
- Clearer code intent (types vs runtime values)
Reference: [TypeScript 3.8 Release Notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export)

View file

@ -0,0 +1,79 @@
---
title: Avoid Object Spread in Hot Loops
impact: LOW-MEDIUM
impactDescription: reduces object allocations by N×
tags: runtime, object-spread, loops, allocation, performance
---
## Avoid Object Spread in Hot Loops
Object spread (`...`) creates a new object on each use. In loops, this causes N object allocations and copies. Mutate objects directly when creating new instances isn't required.
**Incorrect (N object allocations):**
```typescript
function enrichOrders(orders: Order[]): EnrichedOrder[] {
return orders.map(order => ({
...order, // Creates new object
...calculateTotals(order), // Spreads another object
processedAt: new Date()
}))
}
// 10,000 orders = 10,000 object spreads = significant GC pressure
```
**Correct (direct assignment):**
```typescript
interface EnrichedOrder extends Order {
tax: number
shipping: number
total: number
processedAt: Date
}
function enrichOrders(orders: Order[]): EnrichedOrder[] {
return orders.map(order => {
const totals = calculateTotals(order)
return {
id: order.id,
customerId: order.customerId,
items: order.items,
subtotal: order.subtotal,
tax: totals.tax,
shipping: totals.shipping,
total: totals.total,
processedAt: new Date()
}
})
}
```
**Note:** For immutable object creation, explicit property listing is the only spread-free option. This trades verbosity for performance in hot paths. If immutability isn't required, mutating the original object is faster still.
**For accumulation patterns:**
```typescript
// Incorrect - spreads on every iteration
const result = items.reduce((acc, item) => ({
...acc,
[item.id]: item.value
}), {})
// O(n²) - each spread copies growing object
// Correct - mutate accumulator
const result = items.reduce((acc, item) => {
acc[item.id] = item.value
return acc
}, {} as Record<string, number>)
// O(n) - direct property assignment
```
**When spread is acceptable:**
- Outside hot paths
- Small objects (< 10 properties)
- When immutability is required for state management
- When readability significantly improves
Reference: [V8 Object Shapes](https://mathiasbynens.be/notes/shapes-ics)

View file

@ -0,0 +1,65 @@
---
title: Cache Property Access in Loops
impact: LOW-MEDIUM
impactDescription: reduces property lookups by N× in hot paths
tags: runtime, loops, caching, property-access, optimization
---
## Cache Property Access in Loops
Cache deeply nested or polymorphic property access before hot loops. **Note:** Modern V8's inline caches optimize monomorphic access efficiently — this optimization is only meaningful for 10,000+ iterations with deeply nested or polymorphic properties.
**Incorrect (repeated nested access in hot loop):**
```typescript
function processOrders(orders: Order[], config: AppConfig): ProcessedOrder[] {
const results: ProcessedOrder[] = []
for (const order of orders) {
const tax = order.total * config.tax.rate // Nested access each iteration
const shipping = config.shipping.rates[order.region] // Nested access again
results.push({ ...order, tax, shipping, final: order.total + tax + shipping })
}
return results
}
```
**Correct (cached property access):**
```typescript
function processOrders(orders: Order[], config: AppConfig): ProcessedOrder[] {
const results: ProcessedOrder[] = []
const taxRate = config.tax.rate
const shippingRates = config.shipping.rates
for (const order of orders) {
const tax = order.total * taxRate
const shipping = shippingRates[order.region]
results.push({ ...order, tax, shipping, final: order.total + tax + shipping })
}
return results
}
```
**When V8 handles it automatically (no caching needed):**
```typescript
// Monomorphic — all objects have same shape, V8 ICs optimize this
function sumOrders(orders: Order[]): number {
let total = 0
for (let i = 0; i < orders.length; i++) { // orders.length is fine
total += orders[i].total // Same shape every time
}
return total
}
```
**When to skip this optimization:**
- Arrays under 1,000 items
- Monomorphic objects (same shape/class)
- Non-hot paths executed infrequently
- When readability suffers significantly
Reference: [V8 Hidden Classes](https://v8.dev/blog/fast-properties)

View file

@ -0,0 +1,73 @@
---
title: Prefer Native Array Methods Over Lodash
impact: LOW-MEDIUM
impactDescription: eliminates library overhead, enables tree-shaking
tags: runtime, arrays, lodash, native-methods, bundling
---
## Prefer Native Array Methods Over Lodash
Modern JavaScript includes most common array operations. Native methods are faster (no function call overhead) and don't add bundle weight. Use native methods when they provide equivalent functionality.
**Incorrect (lodash for native operations):**
```typescript
import _ from 'lodash' // Imports entire library
const activeUsers = _.filter(users, u => u.isActive)
const userNames = _.map(activeUsers, u => u.name)
const firstAdmin = _.find(users, u => u.role === 'admin')
const hasAdmin = _.some(users, u => u.role === 'admin')
const allActive = _.every(users, u => u.isActive)
const userIds = _.uniq(users.map(u => u.id))
```
**Correct (native methods):**
```typescript
const activeUsers = users.filter(u => u.isActive)
const userNames = activeUsers.map(u => u.name)
const firstAdmin = users.find(u => u.role === 'admin')
const hasAdmin = users.some(u => u.role === 'admin')
const allActive = users.every(u => u.isActive)
const userIds = [...new Set(users.map(u => u.id))]
```
**Native replacements for common Lodash functions:**
```typescript
// _.flatten / _.flattenDeep
const flat = nestedArrays.flat(Infinity)
// _.chunk (still useful from lodash)
function chunk<T>(array: T[], size: number): T[][] {
return Array.from({ length: Math.ceil(array.length / size) }, (_, i) =>
array.slice(i * size, i * size + size)
)
}
// _.groupBy
function groupBy<T>(array: T[], key: keyof T): Record<string, T[]> {
return array.reduce((groups, item) => {
const group = String(item[key])
groups[group] = groups[group] ?? []
groups[group].push(item)
return groups
}, {} as Record<string, T[]>)
}
// Object.groupBy (ES2024)
const grouped = Object.groupBy(users, user => user.role)
// _.pick / _.omit
const { password, ...userWithoutPassword } = user // omit
const { id, name } = user // pick
```
**When Lodash is still valuable:**
- `_.debounce`, `_.throttle` - complex timing logic
- `_.cloneDeep` - deep object cloning
- `_.merge` - deep object merging
- `_.get` with default values (but optional chaining often suffices)
Reference: [You Don't Need Lodash](https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore)

View file

@ -0,0 +1,89 @@
---
title: Use for-of for Simple Iteration
impact: LOW-MEDIUM
impactDescription: reduces iteration boilerplate by 30-50%
tags: runtime, loops, iteration, for-of, readability
---
## Use for-of for Simple Iteration
`for-of` provides clean syntax for array iteration with performance comparable to traditional `for` loops. Use it when you don't need the index and aren't modifying the array.
**Incorrect (index-based when index isn't needed):**
```typescript
function calculateTotal(orders: Order[]): number {
let total = 0
for (let i = 0; i < orders.length; i++) {
total += orders[i].amount
}
return total
}
function processUsers(users: User[]): void {
for (let i = 0; i < users.length; i++) {
sendNotification(users[i])
}
}
```
**Correct (for-of for clean iteration):**
```typescript
function calculateTotal(orders: Order[]): number {
let total = 0
for (const order of orders) {
total += order.amount
}
return total
}
function processUsers(users: User[]): void {
for (const user of users) {
sendNotification(user)
}
}
```
**When to use each pattern:**
```typescript
// for-of: when you only need values
for (const item of items) {
process(item)
}
// forEach: when you want functional style (but can't break/return)
items.forEach(item => process(item))
// for-in: only for object keys (never for arrays)
for (const key in config) {
console.log(key, config[key])
}
// Traditional for: when you need index, or need to modify loop
for (let i = 0; i < items.length; i++) {
if (items[i].id === targetId) {
items[i] = updatedItem // Modifying array
break // Early exit
}
}
// entries(): when you need both index and value
for (const [index, item] of items.entries()) {
console.log(`${index}: ${item.name}`)
}
```
**Avoid for-in for arrays:**
```typescript
// NEVER do this
for (const index in items) {
// index is a string, not number
// Iterates inherited properties
// Wrong order not guaranteed
}
```
Reference: [MDN for...of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of)

View file

@ -0,0 +1,64 @@
---
title: Use Set/Map for O(1) Lookups
impact: LOW-MEDIUM
impactDescription: O(n) to O(1) per lookup
tags: runtime, set, map, lookup, performance
---
## Use Set/Map for O(1) Lookups
Array methods like `.includes()` and `.find()` are O(n) operations. For frequent lookups, convert arrays to Set or Map for O(1) access.
**Incorrect (O(n) per lookup):**
```typescript
const allowedRoles = ['admin', 'editor', 'viewer', 'moderator']
function hasPermission(userRole: string): boolean {
return allowedRoles.includes(userRole) // O(n) every call
}
// In a loop, this becomes O(n × m)
function filterAuthorizedUsers(users: User[]): User[] {
return users.filter(user => allowedRoles.includes(user.role))
// 1000 users × 4 roles = 4000 comparisons
}
```
**Correct (O(1) per lookup):**
```typescript
const allowedRoles = new Set(['admin', 'editor', 'viewer', 'moderator'])
function hasPermission(userRole: string): boolean {
return allowedRoles.has(userRole) // O(1) every call
}
function filterAuthorizedUsers(users: User[]): User[] {
return users.filter(user => allowedRoles.has(user.role))
// 1000 users × O(1) = 1000 operations
}
```
**For object lookups by key:**
```typescript
// Incorrect - O(n) search
const users: User[] = [/* ... */]
function findUserById(id: string): User | undefined {
return users.find(u => u.id === id) // Scans entire array
}
// Correct - O(1) lookup
const userById = new Map<string, User>(users.map(u => [u.id, u]))
function findUserById(id: string): User | undefined {
return userById.get(id)
}
```
**When to stick with arrays:**
- Small collections (< 10 items)
- One-time lookups where conversion cost exceeds benefit
- When you need array methods like `.map()`, `.filter()`, `.slice()`
Reference: [MDN Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set)

View file

@ -0,0 +1,79 @@
---
title: Use Modern String Methods
impact: LOW-MEDIUM
impactDescription: 2-5× faster than regex for simple patterns
tags: runtime, strings, methods, performance, readability
---
## Use Modern String Methods
Modern string methods like `startsWith()`, `endsWith()`, `includes()`, and `padStart()` are clearer and often faster than regex or manual substring operations.
**Incorrect (regex or substring for simple checks):**
```typescript
function isImageFile(filename: string): boolean {
return /\.(jpg|png|gif)$/.test(filename)
}
function hasHttpPrefix(url: string): boolean {
return url.substring(0, 7) === 'http://' || url.substring(0, 8) === 'https://'
}
function containsSearchTerm(text: string, term: string): boolean {
return text.indexOf(term) !== -1
}
function formatOrderId(id: number): string {
return ('000000' + id).slice(-6) // Pad to 6 digits
}
```
**Correct (modern string methods):**
```typescript
function isImageFile(filename: string): boolean {
return filename.endsWith('.jpg') ||
filename.endsWith('.png') ||
filename.endsWith('.gif')
}
function hasHttpPrefix(url: string): boolean {
return url.startsWith('http://') || url.startsWith('https://')
}
function containsSearchTerm(text: string, term: string): boolean {
return text.includes(term)
}
function formatOrderId(id: number): string {
return String(id).padStart(6, '0')
}
```
**Additional useful methods:**
```typescript
// replaceAll (no global regex needed)
const sanitized = input.replaceAll('<', '&lt;').replaceAll('>', '&gt;')
// at() for negative indexing
const lastChar = filename.at(-1) // Last character
const extension = filename.split('.').at(-1) // Last segment
// trimStart/trimEnd for directional trimming
const trimmedLeft = ' text '.trimStart() // 'text '
const trimmedRight = ' text '.trimEnd() // ' text'
// repeat for string multiplication
const separator = '-'.repeat(40)
const indent = ' '.repeat(depth)
```
**When regex is still needed:**
- Complex pattern matching
- Capture groups
- Case-insensitive matching (`/pattern/i`)
- Multiple conditions in one check
Reference: [MDN String Methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)

View file

@ -0,0 +1,92 @@
---
title: Use Assertion Functions for Validation
impact: MEDIUM-HIGH
impactDescription: reduces validation boilerplate by 50-70%
tags: safety, assertion-functions, asserts, validation, narrowing
---
## Use Assertion Functions for Validation
Assertion functions (`asserts` return type) tell TypeScript that if the function returns, the condition is true. This narrows types in the calling scope without explicit if-checks.
**Incorrect (repeated if-throw pattern):**
```typescript
function processOrder(order: Order | null): void {
if (!order) {
throw new Error('Order is required')
}
if (order.status !== 'pending') {
throw new Error('Order must be pending')
}
if (!order.items.length) {
throw new Error('Order must have items')
}
// Finally can use order safely
submitOrder(order)
}
// Same checks repeated in every function that needs a valid order
function shipOrder(order: Order | null): void {
if (!order) throw new Error('Order is required')
if (order.status !== 'pending') throw new Error('Order must be pending')
// ...duplicate validation
}
```
**Correct (assertion function):**
```typescript
interface ValidOrder extends Order {
status: 'pending'
items: [OrderItem, ...OrderItem[]] // Non-empty array
}
function assertValidOrder(order: Order | null): asserts order is ValidOrder {
if (!order) {
throw new Error('Order is required')
}
if (order.status !== 'pending') {
throw new Error('Order must be pending')
}
if (!order.items.length) {
throw new Error('Order must have items')
}
}
function processOrder(order: Order | null): void {
assertValidOrder(order)
// order is now typed as ValidOrder
submitOrder(order) // Type-safe
}
function shipOrder(order: Order | null): void {
assertValidOrder(order)
// Reuses validation, order is ValidOrder
ship(order)
}
```
**For generic assertions:**
```typescript
function assertDefined<T>(value: T | null | undefined, name: string): asserts value is T {
if (value === null || value === undefined) {
throw new Error(`${name} must be defined`)
}
}
function processUser(user: User | null): void {
assertDefined(user, 'user')
// user is now User, not User | null
console.log(user.email)
}
```
**Benefits:**
- Centralizes validation logic
- Automatic type narrowing after assertion
- Clearer intent than if-throw patterns
Reference: [TypeScript 3.7 Assertion Functions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions)

View file

@ -0,0 +1,77 @@
---
title: Use const Assertions for Literal Types
impact: MEDIUM-HIGH
impactDescription: preserves literal types, enables better inference
tags: safety, const-assertion, literals, inference, readonly
---
## Use const Assertions for Literal Types
The `as const` assertion preserves literal types and makes arrays/objects readonly. This enables precise type inference and prevents accidental mutations.
**Incorrect (widened types):**
```typescript
const config = {
apiUrl: 'https://api.example.com',
retries: 3,
methods: ['GET', 'POST']
}
// Type: { apiUrl: string; retries: number; methods: string[] }
function makeRequest(method: 'GET' | 'POST'): void { }
makeRequest(config.methods[0])
// Error: Argument of type 'string' is not assignable to 'GET' | 'POST'
const STATUS = {
PENDING: 'pending',
ACTIVE: 'active'
}
// Type: { PENDING: string; ACTIVE: string }
```
**Correct (const assertion preserves literals):**
```typescript
const config = {
apiUrl: 'https://api.example.com',
retries: 3,
methods: ['GET', 'POST']
} as const
// Type: { readonly apiUrl: 'https://api.example.com'; readonly retries: 3; readonly methods: readonly ['GET', 'POST'] }
function makeRequest(method: 'GET' | 'POST'): void { }
makeRequest(config.methods[0]) // Works: 'GET' is assignable to 'GET' | 'POST'
const STATUS = {
PENDING: 'pending',
ACTIVE: 'active'
} as const
// Type: { readonly PENDING: 'pending'; readonly ACTIVE: 'active' }
type StatusType = typeof STATUS[keyof typeof STATUS] // 'pending' | 'active'
```
**For function parameters:**
```typescript
// Incorrect - tuple becomes array
function setCoordinates(coords: [number, number]): void { }
setCoordinates([10, 20]) // Error: number[] not assignable to [number, number]
// Correct - const preserves tuple
setCoordinates([10, 20] as const) // Works
// Or inline
function setCoordinates(coords: readonly [number, number]): void { }
```
**When to use const assertions:**
- Configuration objects that shouldn't change
- Enum-like objects with string values
- Array/tuple literals passed to functions expecting specific types
- Creating type-safe lookup tables
Reference: [TypeScript 3.4 Const Assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions)

View file

@ -0,0 +1,83 @@
---
title: Use Exhaustive Checks for Union Types
impact: MEDIUM-HIGH
impactDescription: prevents 100% of missing case errors at compile time
tags: safety, exhaustive, never, discriminated-unions, switch
---
## Use Exhaustive Checks for Union Types
Exhaustive checks ensure all union members are handled. When a new member is added, TypeScript errors on unhandled cases rather than falling through silently at runtime.
**Incorrect (missing case compiles but fails at runtime):**
```typescript
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered'
function getStatusMessage(status: OrderStatus): string {
switch (status) {
case 'pending':
return 'Order received'
case 'processing':
return 'Preparing your order'
case 'shipped':
return 'On the way'
// 'delivered' case missing - no compile error
// Returns undefined at runtime
}
}
// Later, someone adds 'cancelled' to OrderStatus
// This function silently returns undefined for 'cancelled' and 'delivered'
```
**Correct (exhaustive check with never):**
```typescript
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered'
function assertNever(value: never): never {
throw new Error(`Unhandled value: ${value}`)
}
function getStatusMessage(status: OrderStatus): string {
switch (status) {
case 'pending':
return 'Order received'
case 'processing':
return 'Preparing your order'
case 'shipped':
return 'On the way'
case 'delivered':
return 'Order complete'
default:
return assertNever(status) // Compile error if case missed
}
}
// Adding 'cancelled' to OrderStatus now causes compile error:
// Argument of type 'string' is not assignable to parameter of type 'never'
```
**For object mapping (alternative pattern):**
```typescript
const statusMessages: Record<OrderStatus, string> = {
pending: 'Order received',
processing: 'Preparing your order',
shipped: 'On the way',
delivered: 'Order complete',
// Missing key causes: Property 'cancelled' is missing in type
}
function getStatusMessage(status: OrderStatus): string {
return statusMessages[status]
}
```
**Benefits:**
- Compile-time error when union expands
- Self-documenting: all cases explicitly handled
- Runtime safety via assertNever fallback
Reference: [TypeScript Handbook - Exhaustiveness Checking](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking)

View file

@ -0,0 +1,85 @@
---
title: Enable noUncheckedIndexedAccess
impact: MEDIUM-HIGH
impactDescription: prevents 100% of unchecked index access errors at compile time
tags: safety, noUncheckedIndexedAccess, strict, arrays, undefined
---
## Enable noUncheckedIndexedAccess
With `noUncheckedIndexedAccess` enabled, TypeScript adds `undefined` to the type of array elements and index signature properties. This catches one of the most common sources of runtime errors — accessing elements that may not exist.
**Incorrect (without noUncheckedIndexedAccess):**
```json
{
"compilerOptions": {
"strict": true
}
}
```
```typescript
const users = ['Alice', 'Bob', 'Charlie']
const first = users[0] // Type: string (lies — could be undefined)
console.log(first.toUpperCase()) // No error, but crashes if array is empty
const scores: Record<string, number> = { math: 95 }
const science = scores['science'] // Type: number (lies — key doesn't exist)
console.log(science.toFixed(2)) // No error, but crashes at runtime
```
**Correct (with noUncheckedIndexedAccess):**
```json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}
```
```typescript
const users = ['Alice', 'Bob', 'Charlie']
const first = users[0] // Type: string | undefined
console.log(first.toUpperCase()) // Error: 'first' is possibly undefined
// Handle the undefined case
if (first) {
console.log(first.toUpperCase()) // OK after narrowing
}
const scores: Record<string, number> = { math: 95 }
const science = scores['science'] // Type: number | undefined
if (science !== undefined) {
console.log(science.toFixed(2)) // OK after narrowing
}
```
**Common patterns with noUncheckedIndexedAccess:**
```typescript
// Array destructuring — first element is T | undefined
const [head, ...rest] = items
if (head) {
processItem(head)
}
// Use non-null assertion only when you've validated
function getRequired(items: string[], index: number): string {
if (index < 0 || index >= items.length) {
throw new RangeError(`Index ${index} out of bounds`)
}
return items[index]! // Safe — bounds checked above
}
// Array.at() returns T | undefined regardless of this flag
const last = items.at(-1) // Already T | undefined
```
**When to disable:**
- Legacy codebases with heavy array indexing (migration cost too high)
- Performance-critical inner loops where the narrowing pattern adds overhead
Reference: [TypeScript tsconfig - noUncheckedIndexedAccess](https://www.typescriptlang.org/tsconfig/noUncheckedIndexedAccess.html)

View file

@ -0,0 +1,76 @@
---
title: Prefer unknown Over any
impact: MEDIUM-HIGH
impactDescription: forces type narrowing, prevents runtime errors
tags: safety, unknown, any, type-narrowing, type-safety
---
## Prefer unknown Over any
The `any` type disables all type checking, allowing unsafe operations to pass silently. Use `unknown` to require explicit type narrowing before operations.
**Incorrect (any bypasses all checks):**
```typescript
function processApiResponse(data: any): string {
return data.user.name.toUpperCase()
// No error even if data is null, has no user, or name isn't a string
// Runtime: TypeError: Cannot read property 'name' of undefined
}
async function fetchData(): Promise<any> {
const response = await fetch('/api/data')
return response.json() // Returns Promise<any>, loses all type info
}
```
**Correct (unknown requires narrowing):**
```typescript
interface ApiResponse {
user: {
name: string
}
}
function isApiResponse(data: unknown): data is ApiResponse {
return (
typeof data === 'object' &&
data !== null &&
'user' in data &&
typeof (data as ApiResponse).user?.name === 'string'
)
}
function processApiResponse(data: unknown): string {
if (!isApiResponse(data)) {
throw new Error('Invalid API response')
}
return data.user.name.toUpperCase() // Type-safe access
}
```
**For JSON parsing:**
```typescript
// Incorrect
const config = JSON.parse(configString) as AppConfig // Unsafe assertion
// Correct
function parseConfig(configString: string): AppConfig {
const parsed: unknown = JSON.parse(configString)
if (!isValidConfig(parsed)) {
throw new Error('Invalid config format')
}
return parsed
}
```
**When any is acceptable:**
- Migrating JavaScript to TypeScript incrementally
- Third-party library workarounds (with `// @ts-expect-error`)
- Truly dynamic code where type is unknowable
Reference: [TypeScript Handbook - Unknown](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-unknown-type)

View file

@ -0,0 +1,72 @@
---
title: Enable strictNullChecks
impact: MEDIUM-HIGH
impactDescription: prevents null/undefined runtime errors
tags: safety, strictNullChecks, null, undefined, strict
---
## Enable strictNullChecks
With `strictNullChecks`, TypeScript distinguishes between `T`, `T | null`, and `T | undefined`. This catches null pointer exceptions at compile time instead of runtime.
**Incorrect (strictNullChecks disabled):**
```typescript
// tsconfig.json: { "strictNullChecks": false }
function getUser(id: string): User {
return userMap.get(id) // Returns User | undefined, but typed as User
}
const user = getUser('123')
console.log(user.email) // No error, but crashes if user is undefined
```
**Correct (strictNullChecks enabled):**
```typescript
// tsconfig.json: { "strict": true } (includes strictNullChecks)
function getUser(id: string): User | undefined {
return userMap.get(id) // Correctly typed as User | undefined
}
const user = getUser('123')
console.log(user.email) // Error: 'user' is possibly 'undefined'
// Must handle the undefined case
if (user) {
console.log(user.email) // Type narrowed to User
}
// Or use optional chaining
console.log(user?.email) // string | undefined
// Or assert when you're certain
const confirmedUser = getUser('123')! // Non-null assertion (use sparingly)
```
**Common patterns with strictNullChecks:**
```typescript
// Default values
function greet(name: string | undefined): string {
return `Hello, ${name ?? 'Guest'}`
}
// Guard clauses
function processOrder(order: Order | null): void {
if (!order) {
throw new Error('Order is required')
}
// order is narrowed to Order
ship(order)
}
// Optional chaining with nullish coalescing
const street = user?.address?.street ?? 'Unknown'
```
**Note:** Always enable `strict: true` which includes `strictNullChecks` along with other safety checks.
Reference: [TypeScript Handbook - Strict Null Checks](https://www.typescriptlang.org/tsconfig#strictNullChecks)

View file

@ -0,0 +1,86 @@
---
title: Use Type Guards for Runtime Type Checking
impact: MEDIUM-HIGH
impactDescription: eliminates type assertions, catches errors at boundaries
tags: safety, type-guards, narrowing, predicates, validation
---
## Use Type Guards for Runtime Type Checking
Type guards provide runtime validation that TypeScript can use for static narrowing. They replace unsafe type assertions with checked operations.
**Incorrect (type assertions without validation):**
```typescript
interface User {
id: string
email: string
role: 'admin' | 'user'
}
function handleUserEvent(event: MessageEvent): void {
const user = event.data as User // Unsafe assertion
sendEmail(user.email) // Crashes if data isn't actually a User
}
function processResponse(data: unknown): User[] {
return data as User[] // No runtime check
}
```
**Correct (type guard with validation):**
```typescript
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as User).id === 'string' &&
typeof (value as User).email === 'string' &&
['admin', 'user'].includes((value as User).role)
)
}
function handleUserEvent(event: MessageEvent): void {
if (!isUser(event.data)) {
console.error('Invalid user data received')
return
}
sendEmail(event.data.email) // Type-safe: event.data is User
}
function processResponse(data: unknown): User[] {
if (!Array.isArray(data)) return []
return data.filter(isUser)
}
```
**For discriminated unions:**
```typescript
interface SuccessResult {
status: 'success'
data: User
}
interface ErrorResult {
status: 'error'
message: string
}
type ApiResult = SuccessResult | ErrorResult
function isSuccess(result: ApiResult): result is SuccessResult {
return result.status === 'success'
}
function handleResult(result: ApiResult): void {
if (isSuccess(result)) {
console.log(result.data.email) // Type narrowed to SuccessResult
} else {
console.error(result.message) // Type narrowed to ErrorResult
}
}
```
Reference: [TypeScript Handbook - Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)

View file

@ -0,0 +1,68 @@
---
title: Enable Incremental Compilation
impact: CRITICAL
impactDescription: 50-90% faster rebuilds
tags: tscfg, incremental, tsconfig, compilation, caching
---
## Enable Incremental Compilation
Incremental compilation caches project graph information between builds in a `.tsbuildinfo` file. Subsequent compilations only recheck changed files and their dependents.
**Incorrect (full rebuild every time):**
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"strict": true
}
}
```
```bash
tsc # 15 seconds every build
```
**Correct (incremental builds):**
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"strict": true,
"incremental": true,
"tsBuildInfoFile": "./dist/.tsbuildinfo"
}
}
```
```bash
tsc # 15s first build, 1-3s subsequent builds
```
**For monorepos (composite projects):**
```json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true
},
"references": [
{ "path": "../shared" },
{ "path": "../utils" }
]
}
```
**Note:** The `composite` flag implies `incremental: true` and requires `declaration: true`.
**When to disable incremental:**
- CI environments where cache isn't preserved between runs
- One-off type-checking scripts
Reference: [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance#incremental-project-emit)

View file

@ -0,0 +1,87 @@
---
title: Use erasableSyntaxOnly for Node.js Native TypeScript
impact: HIGH
impactDescription: prevents 100% of Node.js type-stripping runtime errors
tags: tscfg, erasableSyntaxOnly, node, type-stripping, enums
---
## Use erasableSyntaxOnly for Node.js Native TypeScript
The `erasableSyntaxOnly` flag (TypeScript 5.8+) ensures your code only uses TypeScript syntax that can be removed by erasing type annotations — no code generation required. This is mandatory for Node.js `--experimental-strip-types` which strips types but cannot transform enums, namespaces, or parameter properties.
**Incorrect (non-erasable syntax fails with Node.js type-stripping):**
```json
{
"compilerOptions": {
"erasableSyntaxOnly": true
}
}
```
```typescript
// Error: non-erasable syntax
export enum OrderStatus {
Pending = 'pending',
Shipped = 'shipped',
Delivered = 'delivered'
}
// Error: non-erasable syntax
namespace Validation {
export function isValid(input: string): boolean {
return input.length > 0
}
}
// Error: non-erasable parameter property
class UserService {
constructor(private readonly repository: UserRepository) {}
}
```
**Correct (erasable alternatives):**
```typescript
// Union type instead of enum
export type OrderStatus = 'pending' | 'shipped' | 'delivered'
// Object constant for runtime values
export const OrderStatus = {
Pending: 'pending',
Shipped: 'shipped',
Delivered: 'delivered',
} as const satisfies Record<string, OrderStatus>
// Module-level functions instead of namespace
export function isValid(input: string): boolean {
return input.length > 0
}
// Explicit property assignment instead of parameter property
class UserService {
readonly repository: UserRepository
constructor(repository: UserRepository) {
this.repository = repository
}
}
```
**Recommended configuration for Node.js native TS:**
```json
{
"compilerOptions": {
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"isolatedModules": true
}
}
```
**When NOT to use this flag:**
- Projects using a bundler (esbuild, swc, Vite) that supports enum transformation
- Libraries that need to support both bundled and unbundled consumers
- Codebases with extensive enum usage where migration cost is high
Reference: [TypeScript 5.8 - erasableSyntaxOnly](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-8.html)

View file

@ -0,0 +1,86 @@
---
title: Configure Include and Exclude Properly
impact: CRITICAL
impactDescription: prevents scanning thousands of unnecessary files
tags: tscfg, include, exclude, tsconfig, file-discovery
---
## Configure Include and Exclude Properly
TypeScript walks through all included directories to discover files. Overly broad `include` patterns or missing `exclude` patterns force the compiler to scan irrelevant directories, significantly slowing startup.
**Incorrect (scans entire project tree):**
```json
{
"compilerOptions": {
"outDir": "dist"
},
"include": ["**/*"]
}
```
```bash
# Scans node_modules, dist, coverage, .git...
# Discovery time: 5+ seconds on large projects
```
**Correct (targeted include with explicit exclude):**
```json
{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": [
"node_modules",
"dist",
"coverage",
"**/*.test.ts",
"**/*.spec.ts",
"**/__tests__/**"
]
}
```
```bash
# Only scans src/ directory
# Discovery time: <1 second
```
**For separate test configuration:**
```json
// tsconfig.json (production)
{
"include": ["src/**/*"],
"exclude": ["**/*.test.ts"]
}
// tsconfig.test.json
{
"extends": "./tsconfig.json",
"include": ["src/**/*", "tests/**/*"]
}
```
**Diagnostic commands:**
```bash
# List all files TypeScript will compile
tsc --listFiles
# Explain why each file was included
tsc --explainFiles
```
**Common files to exclude:**
- `node_modules` (always)
- Build output directories (`dist`, `build`, `out`)
- Test files for production builds
- Generated files (`.generated.ts`)
- Coverage reports (`coverage`)
Reference: [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance#configuring-tsconfigjson-or-jsconfigjson)

View file

@ -0,0 +1,79 @@
---
title: Use isolatedModules for Single-File Transpilation
impact: CRITICAL
impactDescription: 80-90% faster transpilation with bundlers
tags: tscfg, isolatedModules, transpilation, bundlers, performance
---
## Use isolatedModules for Single-File Transpilation
The `isolatedModules` flag ensures each file can be transpiled independently, enabling parallel transpilation by bundlers like esbuild, swc, or Babel. This bypasses TypeScript's slower multi-file analysis.
**Incorrect (requires cross-file analysis):**
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext"
}
}
```
```typescript
// constants.ts
export const enum Status {
Active = 'active',
Inactive = 'inactive'
}
// user.ts
import { Status } from './constants'
const status = Status.Active // Requires reading constants.ts to inline
```
**Correct (single-file transpilable):**
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"isolatedModules": true,
"verbatimModuleSyntax": true
}
}
```
```typescript
// constants.ts
export enum Status { // Regular enum, not const enum
Active = 'active',
Inactive = 'inactive'
}
// user.ts
import { Status } from './constants'
const status = Status.Active // Reference preserved, no cross-file read
```
**Build pipeline integration:**
```javascript
// vite.config.ts
export default {
esbuild: {
// esbuild transpiles files in parallel
// TypeScript only runs type-checking
}
}
```
**Code patterns blocked by isolatedModules:**
- `const enum` (use regular `enum` or union types instead)
- `export =` / `import =` syntax
- Re-exporting types without `type` keyword
**Note:** With `erasableSyntaxOnly` (TypeScript 5.8+), regular enums are also blocked. Use union types (`type Status = 'active' | 'inactive'`) as the universal safe alternative. See `tscfg-erasable-syntax-only.md`.
Reference: [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance#isolated-file-emit)

View file

@ -0,0 +1,71 @@
---
title: Enable isolatedDeclarations for Parallel Declaration Emit
impact: CRITICAL
impactDescription: enables parallel .d.ts generation without type-checker
tags: tscfg, isolatedDeclarations, declarations, parallel, performance
---
## Enable isolatedDeclarations for Parallel Declaration Emit
The `isolatedDeclarations` flag (TypeScript 5.5+) ensures each file's exports are annotated sufficiently for tools to generate `.d.ts` files without running the type-checker. This enables parallel declaration emit via bundlers and dramatically speeds up builds in large codebases.
**Incorrect (declaration emit requires full type-check):**
```json
{
"compilerOptions": {
"declaration": true
}
}
```
```typescript
// utils.ts
export function calculateTotal(items: CartItem[]) {
// Return type inferred — requires type-checker to generate .d.ts
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
```
**Correct (explicit annotations enable parallel emit):**
```json
{
"compilerOptions": {
"declaration": true,
"isolatedDeclarations": true
}
}
```
```typescript
// utils.ts
export function calculateTotal(items: CartItem[]): number {
// Explicit return type — .d.ts can be generated per-file, in parallel
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
```
**What requires annotation under isolatedDeclarations:**
- Exported function return types
- Exported variable types when not inferable from a literal
- Exported class method return types
**What does NOT need annotation:**
- Local variables and functions (not exported)
- Function parameters (already required by TypeScript)
- Exports initialized with literals (`export const MAX = 100` is fine)
**Pair with isolatedModules for maximum build speed:**
```json
{
"compilerOptions": {
"isolatedModules": true,
"isolatedDeclarations": true,
"declaration": true
}
}
```
Reference: [TypeScript 5.5 - Isolated Declarations](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html#isolated-declarations)

View file

@ -0,0 +1,97 @@
---
title: Use Project References for Large Codebases
impact: CRITICAL
impactDescription: 60-80% faster incremental builds
tags: tscfg, project-references, monorepo, tsconfig, compilation
---
## Use Project References for Large Codebases
Project references split a codebase into independent compilation units. Each project compiles separately, enabling parallel builds and preventing the compiler from loading the entire codebase at once.
**Incorrect (monolithic tsconfig):**
```text
my-app/
├── tsconfig.json # Single config for entire app
├── packages/
│ ├── api/src/
│ ├── web/src/
│ └── shared/src/
```
```json
{
"compilerOptions": { "outDir": "dist" },
"include": ["packages/*/src/**/*"]
}
```
```bash
# Loads ALL files into memory for every change
# Change in api/ triggers full recompile
```
**Correct (project references):**
```text
my-app/
├── tsconfig.json # Root config with references
├── packages/
│ ├── api/
│ │ └── tsconfig.json # References shared
│ ├── web/
│ │ └── tsconfig.json # References shared
│ └── shared/
│ └── tsconfig.json # No references (leaf)
```
```json
// packages/shared/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}
```
```json
// packages/api/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "dist"
},
"references": [
{ "path": "../shared" }
],
"include": ["src/**/*"]
}
```
```json
// tsconfig.json (root)
{
"files": [],
"references": [
{ "path": "packages/shared" },
{ "path": "packages/api" },
{ "path": "packages/web" }
]
}
```
```bash
tsc --build # Builds only changed projects
```
**Benefits:**
- Parallel compilation of independent projects
- Change in `shared/` only rebuilds dependents
- Declaration files used as API boundaries
Reference: [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance#using-project-references)

View file

@ -0,0 +1,64 @@
---
title: Enable skipLibCheck for Faster Builds
impact: CRITICAL
impactDescription: 20-40% faster compilation
tags: tscfg, skipLibCheck, tsconfig, declaration-files, performance
---
## Enable skipLibCheck for Faster Builds
The `skipLibCheck` option skips type-checking of declaration files (`.d.ts`). Since these files are pre-verified by library authors, checking them is redundant and wastes compilation time.
**Incorrect (checks all declaration files):**
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"strict": true
}
}
```
```bash
# Checks thousands of .d.ts files in node_modules
# Compilation time: 25 seconds
```
**Correct (skips declaration file checks):**
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"strict": true,
"skipLibCheck": true
}
}
```
```bash
# Only checks your source files
# Compilation time: 15 seconds (40% faster)
```
**Alternative (more conservative):**
```json
{
"compilerOptions": {
"skipDefaultLibCheck": true
}
}
```
This only skips checking the default library files (lib.d.ts), not third-party declarations.
**When to disable skipLibCheck:**
- Debugging type conflicts between declaration files
- Publishing a library where you want to verify `.d.ts` output
- Encountering mysterious type errors that might originate in declarations
Reference: [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance#skipping-d-ts-checking)

View file

@ -0,0 +1,65 @@
---
title: Enable strictFunctionTypes for Faster Variance Checks
impact: CRITICAL
impactDescription: enables optimized variance checking
tags: tscfg, strict, strictFunctionTypes, variance, performance
---
## Enable strictFunctionTypes for Faster Variance Checks
With `strictFunctionTypes` enabled, TypeScript uses fast variance-based checking for function parameters. Without it, TypeScript falls back to slower structural comparison for every function type.
**Incorrect (slow structural checking):**
```json
{
"compilerOptions": {
"strict": false,
"strictFunctionTypes": false
}
}
```
```typescript
type Handler<T> = (event: T) => void
// Without strictFunctionTypes, TypeScript uses bidirectional
// (bivariant) checking - comparing structures both ways
const handler: Handler<MouseEvent> = (e: Event) => { } // Allowed but unsafe
```
**Correct (fast variance checking):**
```json
{
"compilerOptions": {
"strict": true
}
}
```
```typescript
type Handler<T> = (event: T) => void
// With strictFunctionTypes, TypeScript uses contravariant
// checking for parameters - faster and type-safe
const handler: Handler<MouseEvent> = (e: Event) => { } // Error: Event is not MouseEvent
```
**Note:** The `strict` flag enables `strictFunctionTypes` along with other strict options. Enable `strict` for all new projects.
**When bivariance is needed:**
```typescript
// Use method syntax for intentional bivariance
interface EventEmitter<T> {
emit(event: T): void // Method syntax = bivariant
}
// vs property syntax for contravariance
interface StrictEmitter<T> {
emit: (event: T) => void // Property syntax = contravariant
}
```
Reference: [TypeScript 2.6 - Strict Function Types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html#strict-function-types)

View file

@ -0,0 +1,81 @@
---
title: Avoid Deeply Nested Generic Types
impact: CRITICAL
impactDescription: prevents exponential instantiation cost
tags: type, generics, nesting, instantiation, performance
---
## Avoid Deeply Nested Generic Types
Each layer of generic nesting multiplies type instantiation cost. Flatten generic hierarchies or use intermediate type aliases to reduce the combinatorial explosion of type checking.
**Incorrect (deeply nested generics):**
```typescript
type ApiResponse<T> = {
data: T
meta: ResponseMeta
}
type PaginatedResponse<T> = ApiResponse<{
items: T[]
pagination: PaginationInfo
}>
type CachedResponse<T> = PaginatedResponse<{
value: T
cachedAt: Date
}>
// Usage creates 4+ levels of nesting
function fetchUsers(): CachedResponse<User> { }
// Compiler must resolve: CachedResponse<User> → PaginatedResponse<...> → ApiResponse<...>
```
**Correct (flattened with composition):**
```typescript
interface PaginationInfo {
page: number
totalPages: number
}
interface CacheInfo {
cachedAt: Date
}
interface PaginatedData<T> {
items: T[]
pagination: PaginationInfo
}
interface ApiResponse<T> {
data: T
meta: ResponseMeta
}
// Compose at usage site instead of nesting
type UserListResponse = ApiResponse<PaginatedData<User> & CacheInfo>
function fetchUsers(): UserListResponse { }
// Single-level generic instantiation
```
**Alternative (builder pattern for complex responses):**
```typescript
interface ResponseBuilder<T> {
data: T
meta: ResponseMeta
}
function withPagination<T>(items: T[], pagination: PaginationInfo): PaginatedData<T> {
return { items, pagination }
}
function withCache<T>(value: T): T & CacheInfo {
return { ...value, cachedAt: new Date() }
}
```
Reference: [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance)

View file

@ -0,0 +1,55 @@
---
title: Avoid Large Union Types
impact: CRITICAL
impactDescription: quadratic O(n²) comparison cost
tags: type, unions, compilation, performance, discriminated-unions
---
## Avoid Large Union Types
Union type checking is quadratic — TypeScript compares each union member pairwise. Unions with 50+ elements cause measurable compilation slowdowns and IDE lag. This commonly occurs with generated types (GraphQL schemas, API response codes, database enums).
**Incorrect (large generated union, O(n²) checks):**
```typescript
// Auto-generated from GraphQL schema — 200+ event types
type AnalyticsEvent =
| 'page_view' | 'button_click' | 'form_submit' | 'scroll_depth'
| 'video_play' | 'video_pause' | 'video_complete' | 'ad_impression'
// ... 200 more event types from analytics schema
// 200 members = 40,000 pairwise comparisons per usage
```
**Correct (branded string type with runtime validation):**
```typescript
type AnalyticsEvent = string & { readonly __brand: 'AnalyticsEvent' }
const VALID_EVENTS = new Set(['page_view', 'button_click', 'form_submit', /* ... */])
function createEvent(name: string): AnalyticsEvent {
if (!VALID_EVENTS.has(name)) {
throw new Error(`Unknown event: ${name}`)
}
return name as AnalyticsEvent
}
```
**For moderately large unions (20-50 members), use discriminated unions:**
```typescript
// Group related values into categories
type UserEvent = { category: 'user'; action: 'login' | 'logout' | 'signup' }
type PageEvent = { category: 'page'; action: 'view' | 'scroll' | 'leave' }
type FormEvent = { category: 'form'; action: 'submit' | 'validate' | 'reset' }
type AppEvent = UserEvent | PageEvent | FormEvent
// Small union of 3 interfaces instead of 9+ string literals
```
**When flat unions are fine:**
- Small unions (< 20 members) have negligible cost
- Unions of primitive literals used in few places
- `string | number | boolean` style utility unions
Reference: [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance#preferring-base-types-over-unions)

View file

@ -0,0 +1,64 @@
---
title: Add Explicit Return Types to Exported Functions
impact: CRITICAL
impactDescription: 30-50% faster declaration emit
tags: type, return-types, exports, inference, performance
---
## Add Explicit Return Types to Exported Functions
Explicit return types accelerate compilation by eliminating inference overhead. Named types are more compact than inferred anonymous types, speeding up declaration file generation and consumption.
**Incorrect (inferred return type, slow declaration emit):**
```typescript
export function fetchUserProfile(userId: string) {
// Compiler must analyze entire function body to infer return type
return fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => ({
id: data.id as string,
name: data.name as string,
email: data.email as string,
createdAt: new Date(data.created_at),
permissions: data.permissions as Permission[],
}))
}
// Inferred: Promise<{ id: string; name: string; email: string; createdAt: Date; permissions: Permission[] }>
```
**Correct (explicit return type, fast compilation):**
```typescript
interface UserProfile {
id: string
name: string
email: string
createdAt: Date
permissions: Permission[]
}
export function fetchUserProfile(userId: string): Promise<UserProfile> {
return fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => ({
id: data.id,
name: data.name,
email: data.email,
createdAt: new Date(data.created_at),
permissions: data.permissions,
}))
}
```
**When to skip explicit return types:**
- Private/internal functions with simple returns
- Arrow functions in local scope
- Functions where the return type is obvious (e.g., `(): void`)
**Benefits:**
- Declaration files use named type instead of expanded inline type
- Faster incremental compilation when function body changes
- Better error messages pointing to return type mismatch
Reference: [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance#using-type-annotations)

View file

@ -0,0 +1,57 @@
---
title: Extract Conditional Types to Named Aliases
impact: CRITICAL
impactDescription: enables compiler caching, prevents re-evaluation
tags: type, conditional-types, generics, caching, performance
---
## Extract Conditional Types to Named Aliases
Inline conditional types are re-evaluated on every function call. Extracting them to named type aliases allows the compiler to cache results and reuse them across multiple call sites.
**Incorrect (inline conditional, re-evaluated each call):**
```typescript
function processResponse<T>(
response: T
): T extends { data: infer D }
? D extends Array<infer Item>
? Item[]
: D
: never {
// Compiler re-computes this complex conditional on every call
return response.data
}
function getFirstItem<T>(collection: T): T extends Array<infer U> ? U : T {
// Re-evaluated for each getFirstItem() usage
}
```
**Correct (extracted, cacheable):**
```typescript
type ExtractData<T> = T extends { data: infer D }
? D extends Array<infer Item>
? Item[]
: D
: never
function processResponse<T>(response: T): ExtractData<T> {
// Compiler caches ExtractData<T> resolution
return response.data
}
type UnwrapArray<T> = T extends Array<infer U> ? U : T
function getFirstItem<T>(collection: T): UnwrapArray<T> {
// Reuses cached UnwrapArray<T> computation
}
```
**Benefits:**
- Type alias acts as a cache boundary
- Reduces duplicate computation across multiple call sites
- Improves IDE responsiveness for autocomplete
Reference: [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance#using-type-aliases)

View file

@ -0,0 +1,41 @@
---
title: Prefer Interfaces Over Type Intersections
impact: CRITICAL
impactDescription: 2-5× faster type resolution
tags: type, interfaces, intersections, compilation, performance
---
## Prefer Interfaces Over Type Intersections
Interfaces create a single flat object type that detects property conflicts at declaration. Intersections recursively merge properties on every use, forcing the compiler to recompute the combined type repeatedly.
**Incorrect (recursive intersection merging):**
```typescript
type UserWithPermissions = User & Permissions & AuditInfo
// Compiler merges all properties on every reference
type ExtendedOrder = Order & {
metadata: OrderMetadata
} & Timestamps
// Each intersection adds another layer of computation
```
**Correct (single flat interface):**
```typescript
interface UserWithPermissions extends User, Permissions, AuditInfo {}
// Single flat type, computed once
interface ExtendedOrder extends Order, Timestamps {
metadata: OrderMetadata
}
// Extends create efficient inheritance chain
```
**When to use intersections:**
- Combining function types or primitives (interfaces cannot extend these)
- Creating mapped or conditional types
- One-off type combinations not reused elsewhere
Reference: [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections)

View file

@ -0,0 +1,65 @@
---
title: Limit Type Recursion Depth
impact: HIGH
impactDescription: prevents exponential type expansion when applicable
tags: type, recursion, generics, depth, performance
---
## Limit Type Recursion Depth
Recursive types without depth limits can cause exponential type expansion, leading to compilation hangs or out-of-memory errors. Add explicit depth counters or use tail-recursive patterns.
**Incorrect (unbounded recursion):**
```typescript
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// No depth limit - deeply nested objects cause exponential expansion
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue }
// Infinite recursion potential
```
**Correct (bounded recursion with depth counter):**
```typescript
type DeepPartial<T, Depth extends number[] = []> = Depth['length'] extends 5
? T // Stop at depth 5
: {
[P in keyof T]?: T[P] extends object
? DeepPartial<T[P], [...Depth, 1]>
: T[P]
}
type JSONValue<Depth extends number[] = []> = Depth['length'] extends 10
? unknown
: | string
| number
| boolean
| null
| JSONValue<[...Depth, 1]>[]
| { [key: string]: JSONValue<[...Depth, 1]> }
```
**Alternative (use built-in utilities):**
```typescript
// For simple cases, prefer built-in Partial over custom DeepPartial
type Config = Partial<AppConfig>
// Use libraries like ts-toolbelt for complex recursive types
// They implement optimized depth-limited versions
```
**When unbounded recursion is acceptable:**
- Types with guaranteed shallow depth (max 2-3 levels)
- Internal types not exposed in public APIs
Reference: [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance)

View file

@ -0,0 +1,71 @@
---
title: Simplify Complex Mapped Types
impact: HIGH
impactDescription: reduces type computation by 50-80% when applicable
tags: type, mapped-types, simplification, utility-types, performance
---
## Simplify Complex Mapped Types
Overly complex mapped types with multiple conditional branches slow compilation significantly. Break them into smaller, focused utility types and compose them.
**Incorrect (monolithic mapped type):**
```typescript
type ComplexTransform<T> = {
[K in keyof T]: T[K] extends Function
? T[K]
: T[K] extends Array<infer U>
? U extends object
? ComplexTransform<U>[]
: T[K]
: T[K] extends object
? T[K] extends Date
? string
: ComplexTransform<T[K]>
: T[K] extends number
? string
: T[K]
}
// Multiple nested conditionals evaluated for every property
```
**Correct (composed utility types):**
```typescript
type TransformValue<T> = T extends Date
? string
: T extends number
? string
: T
type TransformObject<T> = {
[K in keyof T]: TransformProperty<T[K]>
}
type TransformProperty<T> = T extends Function
? T
: T extends Array<infer U>
? TransformArray<U>
: T extends object
? TransformObject<T>
: TransformValue<T>
type TransformArray<T> = T extends object
? TransformObject<T>[]
: T[]
// Each utility is cached independently
type TransformedUser = TransformObject<User>
```
**Benefits:**
- Each small utility type is cached separately
- Easier to debug type errors
- More reusable across the codebase
**When complex mapped types are acceptable:**
- Internal utility types used in few places
- Types that genuinely require complex logic
Reference: [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance)

1
.claude/skills/typescript Symbolic link
View file

@ -0,0 +1 @@
../../.agents/skills/typescript

View file

@ -58,5 +58,19 @@
"unicorn/prefer-ternary": "error",
"unicorn/throw-new-error": "error"
},
"overrides": [
{
"files": ["**/*.ts"],
"rules": {
"max-lines": ["error", { "max": 300, "skipBlankLines": true, "skipComments": true }]
}
},
{
"files": ["**/*.tsx"],
"rules": {
"max-lines": ["error", { "max": 400, "skipBlankLines": true, "skipComments": true }]
}
}
],
"ignorePatterns": ["**/node_modules", "**/dist", "**/out"]
}

View file

@ -10,6 +10,11 @@
"source": "softaworks/agent-toolkit",
"sourceType": "github",
"computedHash": "8c24990b50f431a570f3ad1f60632dee154431861547103047f6ce80092ca734"
},
"typescript": {
"source": "pproenca/dot-skills",
"sourceType": "github",
"computedHash": "3475c8e2a2e556833213dc2d4dff19e09ef166375a203b256b0d7f04f7d5d424"
}
}
}