TypeScript Standards Skill
Shared standards for all TypeScript code in this methodology (backend and frontend).
Strict TypeScript Configuration
All projects must use these TypeScript compiler options:
// tsconfig.json requirements
{
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
Rules:
- All types explicitly declared
- No
anyunless absolutely unavoidable (must be justified) - Prefer
unknownoverany
Immutability (Non-Negotiable)
Use readonly on all properties, ReadonlyArray<T> for arrays, Readonly<T> / ReadonlyMap / ReadonlySet for generic types. Use const exclusively (never let or var). Use spread operators for updates — never mutate.
See immutability.md for full examples and functional alternatives to let.
Banned Mutable Operations
CRITICAL: .push(), .pop(), .shift(), .unshift(), .splice(), .sort(), .reverse(), .fill() on arrays; obj.prop = x, delete obj.prop, Object.assign(target, ...) on objects; .set(), .delete(), .add(), .clear() on Maps/Sets — all strictly forbidden. Use spread operators and immutable patterns instead.
See banned-operations.md for the complete reference tables with alternatives.
Arrow Functions Only
// GOOD: Arrow functions
const createUser = async (deps: Dependencies, args: CreateUserArgs): Promise<CreateUserResult> => {
// ...
};
const handleClick = () => {
// ...
};
// BAD: function keyword
async function createUser(deps: Dependencies, args: CreateUserArgs): Promise<CreateUserResult> {
// ...
}
function handleClick() {
// ...
}
Rule: Use arrow functions exclusively. Never use the function keyword.
No Classes or Inheritance
CRITICAL: Never use classes or inheritance unless creating a subclass of Error.
// GOOD: Types and functions
type User = {
readonly id: string;
readonly email: string;
readonly createdAt: Date;
};
const createUser = (args: CreateUserArgs): User => ({
id: generateId(),
email: args.email,
createdAt: new Date(),
});
// GOOD: Error subclass (only valid use of class)
class ValidationError extends Error {
constructor(
message: string,
readonly field: string,
readonly code: string
) {
super(message);
this.name = 'ValidationError';
}
}
class NotFoundError extends Error {
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`);
this.name = 'NotFoundError';
}
}
// BAD: Classes for domain objects
class User {
constructor(
public id: string,
public email: string
) {}
updateEmail(email: string) {
this.email = email; // Mutation!
}
}
// BAD: Inheritance hierarchies
class Animal { /* ... */ }
class Dog extends Animal { /* ... */ }
// BAD: Service classes
class UserService {
constructor(private db: Database) {}
async createUser(args: CreateUserArgs) { /* ... */ }
}
Why:
- Classes encourage mutation (methods that modify
this) - Inheritance creates tight coupling and fragile hierarchies
- Functions with explicit dependencies are easier to test and reason about
- Error subclasses are the exception because they integrate with JavaScript's error handling (
instanceof, stack traces)
Native JavaScript Only
// GOOD: Native methods
const filtered = users.filter(u => u.active);
const updated = { ...user, email: newEmail };
const mapped = Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, v * 2])
);
// BAD: External utility libraries
import { map } from 'lodash'; // Never
import { produce } from 'immer'; // Never
import * as R from 'ramda'; // Never
Rule: Use only native JavaScript/TypeScript features. No utility libraries like lodash, ramda, or immer.
Why: Reduces bundle size, eliminates dependencies, forces understanding of native methods, ensures code remains maintainable without external library knowledge.
Module System Rules
Named exports only (never default exports). ES modules only (never CommonJS). index.ts files contain only imports/exports (no logic). Always import through index.ts (never bypass to implementation files). Inside a module, never import from its own index.ts — use relative paths to siblings. No file extensions in imports. Use @/ path alias for deep imports (2+ directory levels). Use import type for type-only imports.
See module-system.md for full rules with examples.
Interface vs Type
Rule: interface for function-only contracts (callbacks, loggers, handlers). type for everything else. Data types should not contain functions.
// GOOD: interface for function-only contracts
interface Logger {
readonly info: (message: string, data?: unknown) => void;
readonly warn: (message: string, data?: unknown) => void;
readonly error: (message: string, data?: unknown) => void;
}
// GOOD: type for data shapes
type User = {
readonly id: string;
readonly email: string;
readonly createdAt: Date;
};
type ServerMode = 'api' | 'worker' | 'cron';
type HelmSettings = HelmServerSettings | HelmWebappSettings;
// BAD: interface for data
interface User {
readonly id: string;
readonly email: string;
}
// BAD: type for function contracts
type Logger = {
readonly info: (message: string) => void;
};
// BAD: functions inside data types
type User = {
readonly id: string;
readonly getDisplayName: () => string; // Data types should not have methods
};
Semantic Type Aliases
Use type aliases to give meaning to primitives. A function accepting Milliseconds is self-documenting; a function accepting number is not.
// GOOD: Semantic aliases
type Milliseconds = number;
type Pixels = number;
type DatabaseProvider = 'postgresql';
type ServerMode = 'api' | 'worker' | 'cron';
type SpecStatus = 'pending' | 'in_progress' | 'ready_for_review' | 'approved';
type AnimationConfig = {
readonly duration: Milliseconds;
readonly delay: Milliseconds;
readonly width: Pixels;
};
// BAD: Raw primitives with no meaning
type AnimationConfig = {
readonly duration: number; // Seconds? Milliseconds? Frames?
readonly delay: number;
readonly width: number; // Pixels? Rem? Percent?
};
Advanced Types
Type guards for discriminated union narrowing. as const for literal arrays with typeof X[number] to derive union types. Generics for type-safe operations. keyof and indexed access types for type-safe property access. Object.entries / Object.fromEntries for immutable object transformations.
See advanced-types.md for full examples.
All Functions Must Return Values
Every function should return a meaningful value. Void functions are extremely rare and should be avoided — they hide information from callers and make code harder to compose.
// GOOD: Return a result that callers can use
const saveSettings = async (path: string, settings: SddConfig): Promise<CommandResult> => {
try {
await writeJson(path, settings);
return { success: true, output: `Settings saved to ${path}` };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { success: false, error: `Failed to save settings: ${message}` };
}
};
// GOOD: Even simple operations can return useful info
const addToCache = (cache: ReadonlyMap<string, string>, key: string, value: string): ReadonlyMap<string, string> =>
new Map([...cache, [key, value]]);
// BAD: Void function hides outcome from caller
const saveSettings = async (path: string, settings: SddConfig): Promise<void> => {
await writeJson(path, settings); // Caller can't tell if it worked
};
// BAD: