TypeScript Standards Skill
Shared standards for all TypeScript code in this methodology (backend and frontend).
Shared standards for all TypeScript code in this methodology (backend and frontend).
--- name: typescript-standards description: Shared TypeScript coding standards for strict, immutable, type-safe code. ---
Shared standards for all TypeScript code in this methodology (backend and frontend).
---
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:**
---
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](resources/immutability.md) for full examples and functional alternatives to `let`.
---
**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](resources/banned-operations.md) for the complete reference tables with alternatives.
---
// 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.
---
**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:**
---
// 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.
---
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](resources/module-system.md) for full rules with examples.
---
**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
};---
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?
};---
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](resources/advanced-types.md) for full examples.
---
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: Side-effect-only function
const logMetrics = (data: Metrics): void => {
console.log(JSON.stringify(data));
};**Rule**: All functions should return values. If a function has nothing meaningful to return, that's a signal the design should be reconsidered.
**Exception**: Callback signatures in interface contracts (like `Logger`) may use `void` return types, since the caller doesn't consume the return value. This is the only acceptable use of `void`.
---
Result unions over null — discriminated union return types for failable operations (never return `null` or throw for expected failures). Error catch blocks narrow with `instanceof Error`. External data validated at system boundaries.
See [error-handling.md](resources/error-handling.md) for full patterns with examples.
---
// GOOD: Explicit Promise<T> return type
const loadSettings = async (path: string): Promise<CommandResult> => {
// ...
};
// GOOD: Promise.all for concurrent independent operations
const validationResults = await Promise.all(
specs.map((spec) => validateSpecFile(spec.path))
);
// GOOD: Promise.all with typed results
const results = await Promise.all(
entries.map(async (entry): Promise<ReadonlyArray<string>> => {
if (entry.isDirectory()) {
return walkDir(fullPath, filter);
}
return [];
})
);
// BAD: Sequential when parallel is possible
const result1 = await validateSpec(spec1);
const result2 = await validateSpec(spec2); // Waits unnecessarily
// BAD: Missing return type annotation
const loadSettings = async (path: string) => { // Return type unclear
// ...
};---
For function return types, prefer result unions (see Error Handling). This section covers the distinction between null and undefined in type fields and when interacting with external APIs.
// GOOD: undefined via optional fields ("not provided")
type HelmServerSettings = {
readonly deploy_modes?: ReadonlyArray<ServerMode>; // Optional = may be undefined
};
// GOOD: Handling undefined from native APIs
const first = items.find(i => i.active); // Returns T | undefined natively
if (first === undefined) {
return { success: false, error: 'No active items found' };
}
// GOOD: Handling null from external/DOM APIs
const element = document.getElementById('root'); // Returns HTMLElement | null
if (element === null) {
return { success: false, error: 'Root element not found' };
}
// BAD: Mixing null and undefined in your own types
type Config = {
readonly host: string | null; // Sometimes null
readonly port: string | undefined; // Sometimes undefined
// Inconsistent — use optional fields (undefined) for "not provided"
};
// BAD: Returning null from your own functions
const findComponent = (name: string): Component | null => {
return components.find(c => c.name === name) ?? null;
// Use a result union instead (see Error Handling)
};**Rule**: Use `undefined` (via `?:`) for optional type fields. Handle `null`/`undefined` from external APIs by converting to result unions. Never return `null` from your own functions — use result unions instead.
---
// GOOD: Record<string, never> for placeholder types
type ConfigSettings = Record<string, never>;
type TestingSettings = Record<string, never>;
type CicdSettings = Record<string, never>;
// These can be used in unions without accepting arbitrary data:
type ComponentSettings = ServerSettings | DatabaseSettings | ConfigSettings;
// BAD: Using {} or object for empty types
type ConfigSettings = {}; // Accepts any non-nullish value
type ConfigSettings = object; // Too broad---
// GOOD: ?? for defaults (preserves 0, '', false)
const port = config.port ?? 3000; // Only falls through on null/undefined
const name = config.name ?? 'default'; // '' is a valid name, kept as-is
const verbose = config.verbose ?? false;
// GOOD: || only when 0/''/false should also trigger the default
const displayName = user.nickname || user.email; // Empty string -> use email
// BAD: || when 0 or '' are valid values
const port = config.port || 3000; // port=0 becomes 3000 — wrong!
const count = config.count || 10; // count=0 becomes 10 — wrong!**Rule**: Default to `??`. Only use `||` when you intentionally want to fall through on all falsy values.
---
For detailed guidance, read these on-demand:
---
Before committing TypeScript code, verify:
---
This skill defines no input parameters or structured output.