# Typescript Code Style

> 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.

- Skill: `subsquid/typescript-code-style` (Agent Skill)
- Install (CLI): `npx skillmds@latest add subsquid/typescript-code-style`
- Raw SKILL.md: https://api.skillmd.com/api/skills/subsquid/typescript-code-style/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: subsquid (https://skillmd.com/u/subsquid)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/subsquid/typescript-code-style

---


# TypeScript Code Style

General conventions for all TypeScript projects. Apply these by default.

## TypeScript Configuration

### tsconfig.json

```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

```json
{
  "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:

```ts
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

```ts
/** @internal */
export function internalHelper() {}

export type UserRole = 'admin' | 'user' | 'viewer'

export class UserService {}
```

## Error Handling

### Custom Error Classes

Create specific error types with type guards:

```ts
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

1. **Use type guards** for error discrimination — check both `instanceof` and `name` for cross-boundary errors
2. **Serialize errors with cause chains** in logging using `pino.stdSerializers.errWithCause`
3. **Wrap unknown errors** with `ensureError()` before propagating:
   ```ts
   function ensureError(value: unknown): Error {
     if (value instanceof Error) return value
     return new Error(String(value))
   }
   ```
4. **Never silently swallow errors** — at minimum, log them
5. **Use specific error classes** — not generic `Error` for domain errors

## Configuration

### Environment Variables

Use environment variables for all configuration. Validate at startup:

```ts
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:

```ts
interface HttpClientOptions {
  baseUrl: string
  timeout?: number        // default: 20_000
  retryAttempts?: number  // default: 3
  retrySchedule?: number[] // default: [1000, 3000, 10_000]
  headers?: Record<string, string>
}
```

## Code Patterns

### Prefer `#private` Over `private`

Use ES private fields for true encapsulation:

```ts
class Connection {
  #client: Client
  #logger: Logger

  constructor(client: Client, logger: Logger) {
    this.#client = client
    this.#logger = logger
  }
}
```

### Numeric Literals

Use underscores for readability:

```ts
const MAX_SIZE = 10_485_760   // 10 MB
const TIMEOUT = 5_000         // 5 seconds
const IDLE_TIME = 300         // 300ms
```

### Async Patterns

- Always handle promises — use `@typescript-eslint/no-floating-promises`
- Use `AsyncLocalStorage` for implicit context passing in pipelines:
  ```ts
  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:

```ts
// Good: composable pipeline
const pipeline = source
  .pipe(decode())
  .pipe(transform())
  .pipe(filter())

// Bad: deep inheritance
class SpecializedTransformer extends BaseTransformer extends AbstractTransformer {}
```

### Builder Pattern for Complex Configuration

```ts
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:

```ts
// Good
export function createLogger(options?: LoggerOptions): Logger {
  return pino({ ...defaults, ...options })
}

// Instead of exposing constructor directly
```

## Build Setup

### tsup (for Libraries)

```ts
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)

```json
{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}
```

## Things to Avoid

1. **No `namespace`** — use ES modules
2. **No `enum`** — use `as const` objects or union types:
   ```ts
   // Good
   const Status = { Active: 'active', Inactive: 'inactive' } as const
   type Status = (typeof Status)[keyof typeof Status]

   // Bad
   enum Status { Active = 'active', Inactive = 'inactive' }
   ```
3. **No `any` in public APIs** — use `unknown` and narrow
4. **No `==`** — always `===`
5. **No default exports** — use named exports
6. **No relative imports beyond one level** — use path aliases (`~/`)
7. **No magic numbers** — extract to named constants
8. **No classes for simple data** — use plain objects and types

