typescript-style
TypeScript Code Guidelines
Naming Conventions
- Files and directories:
kebab-case.ts. - Classes, interfaces, types, enums:
PascalCase. - Functions, variables, methods:
camelCase. - Constants:
UPPER_SNAKE_CASE. - Booleans: prefix with
is,has. - Functions: start with, or consist of, a verb (
get,resolve,create). - Use simple variable and function names
const user = getUser().
Type Safety
- No
any. - When the type is genuinely unknown at compile time, use
unknownand narrow via type guards. - Omit explicit return types when TypeScript can infer them unambiguously.
- Prefer
satisfiesoveras, validates the type at the assignment site while preserving the narrower inferred type. - Use
readonlyfor properties that are never reassigned after construction. - Use
as constfor literal objects and arrays that should not change. - Use
import typewhen importing symbols used only as types.
Code Style
Syntax
- 2 spaces for indentation.
- Single quotes for strings.
- Template literals for multi-line strings and interpolation.
- Arrow functions for functions and callbacks.
- Use
constby default. Useletonly when reassignment is required. Novar. - Only
===and!==for comparisons. - Parentheses around return values (Opinionated):
- Variable declarations at the top of their scope.
const work = () => {
const x = 1;
const y = 2;
// Do work.
return (x + y);
};
Imports
- Use ES modules.
- Group imports in the following order:
- Built-in modules (e.g.
fs,path) - External libraries (e.g.
react,lodash) - Internal modules (e.g.
./utils,../components/Button) - Type imports (e.g.
import type { User } from './types') - Multiple imports.
- Built-in modules (e.g.
import { readFile } from 'node:fs';
import { useState } from 'react';
import { debounce } from 'lodash';
import type { Request, Response } from 'express';
import type { Client } from './sdk';
import {
orderByDate,
orderByPrice
} from './components/orders';
import {
User,
Product
} from './models';
- Use engine-specific protocols for module imports:
import { readFile } from 'node:fs';
Structure
- Prefer
async/awaitover.then()chains. - Use early returns to reduce nesting.
- Keep functions small and focused.
- Use higher-order functions (
map,filter,reduce) over imperative loops. - Use one variable per declaration statement.
- Prefer
for...ofwhen iterating objects. - Prefer simple, concise code.
- No default exports, always use named exports.
- Use discriminated unions over optional properties for more complex types.
- Break statements across multiple lines:
// Wrong.
this.emit('agent:status', { state: 'thinking', message: 'Processing your request…' });
// Right.
this.emit('agent:status', {
state: 'thinking',
message: 'Processing your request…'
});
- When using declarative code, for example, using
ZodorDrizzleschemas, write the declarative code by formatting it with each property or method on a new line. For example:
/**
* Schema for validating user data.
*/
const UserSchema = z.object({
/**
* The unique identifier for the user.
* @uuid
*/
id: z
.string()
.uuid()
.describe('The unique identifier for the user.'),
/**
* The name of the user.
* @min 1 character
* @max 100 characters
*/
name: z
.string()
.min(1)
.max(100)
.describe('The name of the user.'),
/**
* The email of the user.
*/
email: z
.string()
.email()
.describe('The email of the user.')
});
Enums vs. Types
Prefer TypeScript type over enum. Use union for simple value sets and as const objects when you need both runtime values and type inference.
// Simple.
type Status = 'active' | 'inactive';
// Multi-line.
type UserRole =
| 'admin'
| 'editor'
| 'viewer'
| 'guest';
Function Chaining
When chaining multiple function calls using a fluent interface:
const result = object
.map(arg1, arg2)
.reduce(arg3)
.pipe(arg4);
Object Literals
When defining object literals, format the code with each property on a new line.
const obj = {
prop1: value1,
prop2: value2,
prop3: value3
};
Curly Braces
Always use curly braces for blocks.
For example:
// Wrong.
if (condition) order();
// Wrong.
if (condition)
order();
// Right.
if (condition) {
order();
}
Organization
- Types are always co-located with their implementation and define a single type, or multiple closely related types. Avoid "types-only" files that export unrelated types.
- Organize code by feature or module under specific directories.
- Avoid global type augmentation when possible.
- Store code in a
src/directory, with subdirectories for features or modules.
Comments
- Use JSDoc comments for functions, types, enums, classes, methods, and class properties. Class properties are always defined at the top of the class, before the constructor. Example:
/**
* Calculates the area of a rectangle.
* @param width - The width of the rectangle.
* @param height - The height of the rectangle.
* @returns The area of the rectangle.
*/
const calculateArea = (width: number, height: number) => {
return (width * height);
};
/**
* Properties for a custom order.
*/
export type CustomOrderProps = {
/**
* The unique identifier for the order.
*/
id: string;
/**
* The product being ordered.
*/
product: string;
/**
* The quantity to be ordered.
*/
quantity: number;
};
/**
* The `CustomOrder` class represents a custom order in the system.
*/
export class CustomOrder {
/**
* Creates a new custom order.
* @param props - The properties of the custom order.
* @constructor
*/
constructor(private props: CustomOrderProps) {}
/**
* Computes the total price of the order.
* @returns The total price of the order.
*/
getTotalPrice() {
// Implementation goes here.
}
}
- Only use
@typein JSDoc comments when the type or unit is non-obvious.
/**
* The default timeout for API requests.
* @type {number} in milliseconds
*/
const DEFAULT_TIMEOUT = 5_000;
-
Do not use decorative separators (e.g.,
-,// --------,// ========) in comments. -
Use inline comments to explain what the code is doing and why.
/**
* Issues an API request to the given URL and returns the response data.
* @param url - The URL to send the request to.
* @returns The response data from the API.
*/
const request = async (url: URL) => {
const response = await fetch(url);
// Check for successful response.
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Validate response with Zod.
const result = ResponseSchema.safeParse(
await response.json()
);
if (!result.success) {
throw new Error(`Invalid response format: ${result.error.message}`);
}
return (result.data);
};
- Do not comment the top of files with a description of the file's purpose.
- Do not use single-line block comments (
/** ... */) for function or type declarations. Use multi-line JSDoc comments instead.
Globals
Use multi-line comments for global variables or constants. Globals should be defined at the top of the file, after imports and before any other code.
/**
* The default timeout for API requests.
* @type {number} in milliseconds
*/
const DEFAULT_TIMEOUT = 5_000;
Performance
- Prefer
MapandSetover plain objects for frequent lookups. - Avoid unnecessary spread copies in hot paths.
- Use
Promise.all()for independent async operations;Promise.allSettled()when dealing with partial failure.
Anti-Patterns
- No
ObjectorArrayconstructors — use literals{}and[]. - No
eval()orFunctionconstructor. - No
withstatements. - No deep nesting of callbacks or conditionals.
- Avoid non-null assertions (
!). - No over-annotation of inferred types when not necessary.
- Avoid Barrel files as they break tree-shaking, slow down bundlers and IDE indexing, create circular dependency traps, and inflate bundle sizes.
- No static-only classes.
- No nested ternaries — max one level deep.
- No magic numbers / strings — extract to named constants.
- No prototype manipulation.
- No unbounded or user-supplied regular expressions — avoid catastrophic backtracking (ReDoS). Prefer simple patterns, use non-backtracking quantifiers, or validate regex complexity before execution.
Logging
See the reference guide for details.
Architecture
See the reference guide for details.
Security
See the reference guide for details.
Testing
- Use
vitestfor unit and integration testing. - Use supertest for HTTP endpoint testing when a headless browser is not needed.
- Write tests per feature or component in a dedicated
tests/directory within each associated package. For monorepos, each package should have its owntests/directory and test configuration. - Under the
tests/directory, organize tests by type (e.g.,unit/,integration/,e2e/).
Environment
For every project using environment variables, always use an env.ts file that defines and exports all expected environment variables with proper types. Use zod to validate these variables at runtime.
/**
* Environment variable schema definition.
*/
const envSchema = z.object({
/**
* The Supabase region where the function is executing.
* Automatically set by Supabase runtime.
* @optional Available only in deployed environments
*/
SB_REGION: z
.string()
.optional(),
/**
* Unique identifier for the current function execution.
* Automatically set by Supabase runtime.
* @optional Available only in deployed environments
*/
SB_EXECUTION_ID: z
.string()
.optional(),
/**
* The current environment (development, staging, production).
* Used for conditional logic and logging.
* @default "development"
*/
ENVIRONMENT: z
.enum(['development', 'staging', 'production'])
.default('development')
});
/**
* Parse and validate environment variables.
* @throws {z.ZodError} If validation fails, with detailed error messages
* @returns Validated and typed environment variables
*/
const parseEnv = (): Env => {
try {
return envSchema.parse({
SB_REGION: process.env.SB_REGION,
SB_EXECUTION_ID: process.env.SB_EXECUTION_ID,
ENVIRONMENT: process.env.ENVIRONMENT
});
} catch (error) {
// Log the error with details for debugging.
throw error;
}
};
/**
* Validated environment variables.
*
* Import this constant to access typed, validated environment variables
* throughout your Edge Functions.
*/
export const env = parseEnv();
Tooling
Code Quality & Type Safety
| Tool | Purpose |
|---|---|
Zod |
Runtime validation and type inference for complex data structures. |
oxlint |
Code linter. |
oxfmt |
Code formatter. |
tsc |
TypeScript compiler for type checking and transpilation. |
tsx |
TypeScript execution environment for running TS files directly. |
Tests
| Tool | Purpose |
|---|---|
vitest |
Fast and lightweight testing framework with built-in TypeScript support. |
supertest |
HTTP assertions for testing API endpoints without a headless browser. |
HTTP Stress Testing
| Tool | Purpose |
|---|---|
wrk |
High-performance HTTP benchmarking tool for load testing APIs. |
k6 |
Modern load testing tool with scripting in JavaScript/TypeScript. |
autocannon |
Fast HTTP/1.1 benchmarking tool for stress testing APIs. |
Web Page Testing
| Tool | Purpose |
|---|---|
playwright |
End-to-end testing framework for web applications with TypeScript support. |
puppeteer |
Headless Chrome Node API for web page testing and automation. |
Database & ORMs
| Need | Use | Why |
|---|---|---|
| ORM | Drizzle | SQL-like API, fully type-safe, zero overhead, supports Postgres/MySQL/SQLite |
| Query builder | Kysely | Type-safe raw SQL builder, no magic, great for complex queries |
Zod-specific Guidelines
- Define schemas in dedicated files.
- Use
z.infer<typeof Schema>to derive types from Zod schemas. - Use
.describe()to add descriptions to schema properties.
Package Managers
| Package Manager | Use Case |
|---|---|
pnpm |
Default for Node.js projects. |
bun |
Use when the project is Bun-based. |
npm |
Use when the project is Node.js-based and there's a specific reason to choose npm. |
Files
Add the following files to your project.
| File | Purpose |
|---|---|
.oxfmtrc.json |
Oxlint configuration in references/config/ |
.oxlintrc.json |
Oxfmt configuration in references/config/ |
tsconfig.json |
TypeScript configuration in references/config/. |
.gitignore |
Specifies files and directories to ignore in version control. |
nx.json or turbo.json |
Configuration for monorepo management with Nx or Turborepo. |
.nvmrc |
Specifies the Node.js version for the project. |
CI/CD
| Tool | Purpose |
|---|---|
GitHub Actions |
CI/CD pipelines for automated testing, linting, and deployment. |
tsc --noEmit |
Type-check step — run in CI to catch type errors before merge. |
vitest run |
Run tests. |
Reference Files
The references/config directory contains starter configurations:
.oxlintrc.json— Oxlint configuration with rules for TypeScript projects..oxfmtrc.json— Oxfmt configuration for consistent code formatting.tsconfig.json— TypeScript compiler options withstrict: true.