TypeScript House Style
Overview
Comprehensive TypeScript coding standards emphasizing type safety, immutability, and integration with Functional Core, Imperative Shell (FCIS) pattern.
Core principles:
- Types as documentation and constraints
- Immutability by default prevents bugs
- Explicit over implicit (especially in function signatures)
- Functional Core returns Results, Imperative Shell may throw
- Configuration over decoration/magic
Quick Self-Check (Use Under Pressure)
When under deadline pressure or focused on other concerns (performance, accuracy, features), STOP and verify:
- Using
Array<T>notT[] - Using
typenotinterface(unless class contract) - Using math.js for money/currencies/complex math
- Parameters are
readonlyorReadonly<T> - Using
unknownnotany - Using
nullfor absent values (notundefined) - Using function declarations (not const arrow) for top-level functions
- Using named exports (not default exports)
- Using
===not== - Using
.sort((a, b) => a - b)for numeric arrays - Using
parseInt(x, 10)with explicit radix
Why this matters: Under pressure, you'll default to muscle memory. These checks catch the most common violations.
Type Declarations
Type vs Interface
Always use type except for class contracts.
// GOOD: type for object shapes
type UserData = {
readonly id: string;
name: string;
email: string | null;
};
// GOOD: interface for class contract
interface IUserRepository {
findById(id: string): Promise<User | null>;
}
class UserRepository implements IUserRepository {
// implementation
}
// BAD: interface for object shape
interface UserData {
id: string;
name: string;
}
Rationale: Types compose better with unions and intersections, support mapped types, and avoid declaration merging surprises. Interfaces are only for defining what a class must implement.
IMPORTANT: Even when under deadline pressure, even when focused on other concerns (financial accuracy, performance optimization, bug fixes), take 2 seconds to ask: "Is this a class contract?" If no, use type. Don't default to interface out of habit.
Naming Conventions
Type Suffixes
| Suffix | Usage | Example |
|---|---|---|
FooOptions | Function parameter objects (3+ args or any optional) | ProcessUserOptions |
FooConfig | Persistent configuration from storage | DatabaseConfig |
FooResult | Discriminated union return types | ValidationResult |
FooFn | Function/callback types | TransformFn<T> |
FooProps | React component props | ButtonProps |
FooState | State objects (component/application) | AppState |
General Casing
| Element | Convention | Example |
|---|---|---|
| Variables & functions | camelCase | userName, getUser() |
| Types & classes | PascalCase | UserData, UserService |
| Constants | UPPER_CASE | MAX_RETRY_COUNT, API_ENDPOINT |
| Files | kebab-case | user-service.ts, process-order.ts |
Boolean Naming
Use is/has/can/should/will prefixes. Avoid negative names.
// GOOD
const isActive = true;
const hasPermission = checkPermission();
const canEdit = user.role === 'admin';
const shouldRetry = attempts < MAX_RETRIES;
const willTimeout = elapsed > threshold;
// Also acceptable: adjectives for state
type User = {
active: boolean;
visible: boolean;
disabled: boolean;
};
// BAD: negative names
const isDisabled = false; // prefer isEnabled
const notReady = true; // prefer isReady
Type Suffix Details
FooOptions - Parameter Objects
Use for functions with 3+ arguments OR any optional arguments.
type ProcessUserOptions = {
readonly name: string;
readonly email: string;
readonly age: number;
readonly sendWelcome?: boolean;
};
// GOOD: destructure in body, not in parameters
function processUser(options: ProcessUserOptions): void {
const {name, email, age, sendWelcome = true} = options;
// implementation
}
// BAD: inline destructuring in parameters
function processUser({name, email, age}: {name: string, email: string, age: number}) {
// causes duplication when destructuring
}
// BAD: not using options pattern for 3+ args
function processUser(name: string, email: string, age: number, sendWelcome?: boolean) {
// hard to call, positional arguments
}
FooResult - Discriminated Unions
Always use discriminated unions for Result types. Integrate with neverthrow.
// GOOD: discriminated union with success/error
type ValidationResult =
| { success: true; data: ValidUser }
| { success: false; error: ValidationError };
// GOOD: use neverthrow for Result types
import {Result, ok, err} from 'neverthrow';
type ValidationError = {
field: string;
message: string;
};
function validateUser(data: Readonly<UserData>): Result<ValidUser, ValidationError> {
if (!data.email) {
return err({field: 'email', message: 'Email is required'});
}
return ok({...data, validated: true});
}
// Usage
const result = validateUser(userData);
if (result.isOk()) {
console.log(result.value); // ValidUser
} else {
console.error(result.error); // ValidationError
}
Rule: Functional Core functions should return Result<T, E> types. Imperative Shell functions may throw exceptions for HTTP errors and similar.
Functions
Declaration Style
Use function declarations for top-level functions. Use arrow functions for inline callbacks.
// GOOD: function declaration for top-level
function processUser(data: Readonly<UserData>): ProcessResult {
return {success: true, user: data};
}
// GOOD: arrow functions for inline callbacks
const users = rawData.map(u => transformUser(u));
button.addEventListener('click', (e) => handleClick(e));
fetch(url).then(data => processData(data));
// BAD: const arrow for top-level function
const processUser = (data: UserData): ProcessResult => {
return {success: true, user: data};
};
Rationale: Function declarations are hoisted and more visible. Arrow functions capture lexical this and are concise for callbacks.
Const Arrow Functions
Use const foo = () => {} declarations only for stable references.
// GOOD: stable reference for React hooks
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
// implementation
};
useEffect(() => {
// handleSubmit reference is stable
}, [handleSubmit]);
// GOOD: long event listener passed from variable
const handleComplexClick = (event: MouseEvent) => {
// many lines of logic
};
element.addEventListener('click', handleComplexClick);
// BAD: const arrow for regular top-level function
const calculateTotal = (items: Array<Item>): number => {
return items.reduce((sum, item) => sum + item.price, 0);
};
// GOOD: use function declaration
function calculateTotal(items: ReadonlyArray<Item>): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
Parameter Objects
Use parameter objects for 3+ arguments OR any optional arguments.
// GOOD: options object for 3+ args
type CreateUserOptions = {
readonly name: string;
readonly email: string;
readonly age: number;
readonly newsletter?: boolean;
};
function createUser(options: CreateUserOptions): User {
const {name, email, age, newsletter = false} = options;
// implementation
}
// GOOD: 2 args, but one is optional - use options
type SendEmailOptions = {
readonly to: string;
readonly subject: string;
readonly body?: string;
};
function sendEmail(options: SendEmailOptions): void {
// implementation
}
// GOOD: 2 required args - no options needed
function divide(numerator: number, denominator: number): number {
return numerator / denominator;
}
Async Functions
Always explicitly type Promise returns. Avoid async void.
// GOOD: explicit