| name | typescript-code-style |
| description | Use when writing TypeScript code in any project. Covers naming conventions, file organization, error handling, configuration patterns, TypeScript compiler settings, and general code approaches. Apply these conventions by default. |
TypeScript Code Style
General conventions for all TypeScript projects. Apply these by default.
TypeScript Configuration
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"noImplicitReturns": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"sourceMap": true,
"outDir": "dist",
"stripInternal": true
}
}
Key points:
- Always enable
strict: true
- Use
nodenext module resolution for Node.js projects
- Use
stripInternal: true with @internal JSDoc to hide implementation details from declaration files
Path Aliases
{
"compilerOptions": {
"paths": {
"~/*": ["src/*"]
}
}
}
Use ~/ prefix for internal imports. Never use relative paths that go up more than one level (../../).
Naming Conventions
| Element | Convention | Example |
|---|
| Files | kebab-case | user-service.ts |
| Classes | PascalCase | UserService |
| Interfaces/Types | PascalCase | UserSession |
| Functions/Methods | camelCase | findByEmail() |
| Constants | UPPER_SNAKE_CASE | MAX_RETRY_COUNT |
| Private class fields | # prefix | #connection |
| Enum values | PascalCase | Status.Active |
| Boolean variables | is/has/can prefix | isActive, hasPermission |
File Naming
user.service.ts # service
user.controller.ts # controller
user.repository.ts # repository
user.entity.ts # database entity
user.test.ts # test
user.e2e.test.ts # e2e test
create-user.request.ts # request DTO
user.response.ts # response DTO
index.ts # barrel exports
Exports
Barrel Files
Use index.ts for public API of a module:
export * from './user.service'
export * from './user.entity'
export type { UserSession } from './user.types'
Export Patterns
- Use
export type for type-only exports
- Mark internal implementation with
@internal JSDoc
- Prefer named exports over default exports
export function internalHelper() {}
export type UserRole = 'admin' | 'user' | 'viewer'
export class UserService {}
Error Handling
Custom Error Classes
Create specific error types with type guards:
export class AppError extends Error {
constructor(
message: string,
public readonly code: string,
) {
super(message)
this.name = 'AppError'
}
}
export class ForkException extends Error {
readonly name = 'ForkError'
readonly previousBlocks: BlockCursor[]
constructor(previousBlocks: BlockCursor[]) {
super(`Fork detected at block ${previousBlocks[0]?.number}`)
this.previousBlocks = previousBlocks
}
}
export function isForkException(err: unknown): err is ForkException {
return err instanceof ForkException || (err instanceof Error && err.name === 'ForkError')
}
Error Handling Rules
- Use type guards for error discrimination — check both
instanceof and name for cross-boundary errors
- Serialize errors with cause chains in logging using
pino.stdSerializers.errWithCause
- Wrap unknown errors with
ensureError() before propagating:
function ensureError(value: unknown): Error {
if (value instanceof Error) return value
return new Error(String(value))
}
- Never silently swallow errors — at minimum, log them
- Use specific error classes — not generic
Error for domain errors
Configuration
Environment Variables
Use environment variables for all configuration. Validate at startup:
const config = {
port: Number(process.env.HTTP_PORT ?? 3000),
logLevel: process.env.LOG_LEVEL ?? 'info',
databaseUrl: requireEnv('DATABASE_URL'),
}
function requireEnv(name: string): string {
const value = process.env[name]
if (!value) throw new Error(`Missing required env var: ${name}`)
return value
}
Options Objects
Use options objects for component configuration with sensible defaults:
interface HttpClientOptions {
baseUrl: string
timeout?: number
retryAttempts?: number
retrySchedule?: number[]
headers?: Record<string, string>
}
Code Patterns
Prefer #private Over private
Use ES private fields for true encapsulation:
class Connection {
#client: Client
#logger: Logger
constructor(client: Client, logger: Logger) {
this.#client = client
this.#logger = logger
}
}
Numeric Literals
Use underscores for readability:
const MAX_SIZE = 10_485_760
const TIMEOUT = 5_000
const IDLE_TIME = 300
Async Patterns
- Always handle promises — use
@typescript-eslint/no-floating-promises
- Use
AsyncLocalStorage for implicit context passing in pipelines:
import { AsyncLocalStorage } from 'node:async_hooks'
const asyncLocalStorage = new AsyncLocalStorage<RuntimeContext>()
export function runWithContext<T>(ctx: RuntimeContext, fn: () => Promise<T>): Promise<T> {
return asyncLocalStorage.run(ctx, fn)
}
export function useContext(): RuntimeContext {
const ctx = asyncLocalStorage.getStore()
if (!ctx) throw new Error('No runtime context')
return ctx
}
Composition Over Inheritance
Use composable transformers/middleware instead of deep class hierarchies:
const pipeline = source
.pipe(decode())
.pipe(transform())
.pipe(filter())
class SpecializedTransformer extends BaseTransformer extends AbstractTransformer {}
Builder Pattern for Complex Configuration
const query = evmQueryBuilder()
.addFields({ logs: { address: true, topics: true } })
.setRange({ from: 1_000_000 })
.build()
Factory Functions Over Constructors
Prefer factory functions when the construction logic is complex or return types vary:
export function createLogger(options?: LoggerOptions): Logger {
return pino({ ...defaults, ...options })
}
Build Setup
tsup (for Libraries)
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
outDir: 'dist',
format: ['cjs', 'esm'],
sourcemap: true,
dts: true,
clean: true,
})
Package.json Exports (Dual CJS/ESM)
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}
Things to Avoid
- No
namespace — use ES modules
- No
enum — use as const objects or union types:
const Status = { Active: 'active', Inactive: 'inactive' } as const
type Status = (typeof Status)[keyof typeof Status]
enum Status { Active = 'active', Inactive = 'inactive' }
- No
any in public APIs — use unknown and narrow
- No
== — always ===
- No default exports — use named exports
- No relative imports beyond one level — use path aliases (
~/)
- No magic numbers — extract to named constants
- No classes for simple data — use plain objects and types