Use when writing TypeScript code, reviewing TS implementations, or making decisions about type declarations, function styles, or naming conventions - comprehensive house style covering type vs interface rules, function declarations, FCIS integration, immutability patterns, and type safety enforcement
Comprehensive TypeScript coding standards emphasizing type safety, immutability, and integration with Functional Core, Imperative Shell (FCIS) pattern.
Core principles:
When under deadline pressure or focused on other concerns (performance, accuracy, features), STOP and verify:
Array<T> not T[]type not interface (unless class contract)readonly or Readonly<T>unknown not anynull for absent values (not undefined)=== not ==.sort((a, b) => a - b) for numeric arraysparseInt(x, 10) with explicit radixWhy this matters: Under pressure, you'll default to muscle memory. These checks catch the most common violations.
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.
| 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 |
| 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 |
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
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
}
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.
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.
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);
}
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;
}
Always explicitly type Promise returns. Avoid async void.
// GOOD: explicit Promise return type
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// GOOD: Promise<void> for side effects
async function saveUser(user: User): Promise<void> {
await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(user),
});
}
// BAD: implicit return type
async function fetchUser(id: string) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
Prefer async/await over .then() chains.
// GOOD: async/await
async function processUserData(id: string): Promise<ProcessedUser> {
const user = await fetchUser(id);
const enriched = await enrichUserData(user);
return transformUser(enriched);
}
// BAD: promise chains
function processUserData(id: string): Promise<ProcessedUser> {
return fetchUser(id)
.then(user => enrichUserData(user))
.then(enriched => transformUser(enriched));
}
Be selective with async. Not everything needs to be async. Sync code is simpler to reason about and debug.
Use async for:
Stay sync for:
// GOOD: sync for pure transformation
function transformUser(user: User): TransformedUser {
return {
fullName: `${user.firstName} ${user.lastName}`,
email: user.email.toLowerCase(),
};
}
// GOOD: async for I/O
async function loadAndTransformUser(id: string): Promise<TransformedUser> {
const user = await fetchUser(id);
return transformUser(user); // Sync call inside async function is fine
}
// BAD: unnecessary async
async function transformUser(user: User): Promise<TransformedUser> {
return {
fullName: `${user.firstName} ${user.lastName}`,
email: user.email.toLowerCase(),
};
}
Why this matters: Async adds complexity—error propagation, cleanup, and stack traces become harder to follow. Keep the async boundary as close to the I/O as possible.
Prefer functions over classes, EXCEPT for dependency injection patterns.
// GOOD: class as dependency container
class UserService {
constructor(
private readonly db: Database,
private readonly logger: Logger,
private readonly cache: Cache,
) {}
async getUser(id: string): Promise<User | null> {
this.logger.info(`Fetching user ${id}`);
const cached = await this.cache.get(`user:${id}`);
if (cached) return cached;
const user = await this.db.users.findById(id);
if (user) await this.cache.set(`user:${id}`, user);
return user;
}
}
// BAD: class with no dependencies
class MathUtils {
add(a: number, b: number): number {
return a + b;
}
}
// GOOD: plain functions
function add(a: number, b: number): number {
return a + b;
}
Use constructor injection into private readonly fields.
// GOOD: constructor injection, private readonly
class OrderProcessor {
constructor(
private readonly orderRepo: OrderRepository,
private readonly paymentService: PaymentService,
private readonly notifier: NotificationService,
) {}
async processOrder(orderId: string): Promise<void> {
const order = await this.orderRepo.findById(orderId);
// implementation
}
}
// BAD: public mutable fields
class OrderProcessor {
public orderRepo: OrderRepository;
public paymentService: PaymentService;
constructor(orderRepo: OrderRepository, paymentService: PaymentService) {
this.orderRepo = orderRepo;
this.paymentService = paymentService;
}
}
Use this only in class methods. Avoid elsewhere.
// GOOD: this in class method
class Counter {
private count = 0;
increment(): void {
this.count++;
}
}
// BAD: this in object literal
const counter = {
count: 0,
increment() {
this.count++; // fragile, breaks when passed as callback
},
};
// GOOD: closure over variable
function createCounter() {
let count = 0;
return {
increment: () => count++,
getCount: () => count,
};
}
Always explicit in function signatures. Infer in local variables, loops, destructuring, and intermediate calculations.
// GOOD: explicit function signature, inferred locals
function processUsers(users: ReadonlyArray<User>): Array<ProcessedUser> {
const results: Array<ProcessedUser> = [];
for (const user of users) { // user inferred as User
const name = user.name; // name inferred as string
const upper = name.toUpperCase(); // upper inferred as string
const processed = {id: user.id, name: upper}; // processed inferred
results.push(processed);
}
return results;
}
// GOOD: destructuring with inference
function formatUser({name, email}: User): string {
return `${name} <${email}>`;
}
// BAD: missing return type
function processUsers(users: ReadonlyArray<User>) {
// ...
}
// BAD: excessive annotations on locals
function processUsers(users: ReadonlyArray<User>): Array<ProcessedUser> {
const results: Array<ProcessedUser> = [];
for (const user: User of users) {
const name: string = user.name;
const upper: string = name.toUpperCase();
// ...
}
return results;
}
Mark reference type parameters as Readonly<T>. Use const for all bindings unless mutation needed.
// GOOD: readonly parameters
function processData(
data: Readonly<UserData>,
config: Readonly<ProcessConfig>,
): ProcessResult {
// data and config cannot be mutated
return {success: true};
}
// GOOD: const bindings
function calculateTotal(items: ReadonlyArray<Item>): number {
const taxRate = 0.08;
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
const tax = subtotal * taxRate;
return subtotal + tax;
}
// BAD: mutable parameters
function processData(data: UserData, config: ProcessConfig): ProcessResult {
data.processed = true; // mutation
return {success: true};
}
ALWAYS use Array<T> or ReadonlyArray<T>. NEVER use T[] syntax.
// GOOD: Array<T> syntax
const numbers: Array<number> = [1, 2, 3];
const roles: Array<UserRole> = ['admin', 'editor'];
function calculateAverage(numbers: ReadonlyArray<number>): number {
return numbers.reduce((a, b) => a + b, 0) / numbers.length;
}
// BAD: T[] syntax (don't use this even if common in examples)
const numbers: number[] = [1, 2, 3]; // NO
const roles: UserRole[] = ['admin']; // NO
function calculateAverage(numbers: number[]): number { // NO
// ...
}
Why: Consistency with other generic syntax. Array<T> is explicit and matches ReadonlyArray<T>, Record<K, V>, Promise<T>, etc. The T[] syntax is muscle memory from other languages but inconsistent with TypeScript's generic patterns.
Prefer readonly outside local scope:
// GOOD: readonly array for function parameter
function calculateAverage(numbers: ReadonlyArray<number>): number {
return numbers.reduce((a, b) => a + b, 0) / numbers.length;
}
// GOOD: mutable array in local scope
function processItems(items: ReadonlyArray<Item>): Array<ProcessedItem> {
const results: Array<ProcessedItem> = [];
for (const item of items) {
results.push(transformItem(item));
}
return results;
}
Use Readonly<T> for shallow immutability, ReadonlyDeep<T> from type-fest when you need immutability all the way down.
import type {ReadonlyDeep} from 'type-fest';
// GOOD: shallow readonly for flat objects
type UserData = Readonly<{
id: string;
name: string;
email: string;
}>;
// GOOD: deep readonly for nested structures
type AppConfig = ReadonlyDeep<{
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
features: {
enabled: Array<string>;
};
}>;
function loadConfig(config: AppConfig): void {
// config is deeply immutable
// config.database.credentials.username = 'x'; // ERROR
}
ALWAYS use math.js for:
NEVER use JavaScript number for:
import { create, all, MathJsInstance } from 'mathjs';
const math: MathJsInstance = create(all);
// GOOD: math.js for currency calculations
function calculateTotal(
price: number,
quantity: number,
taxRate: number
): string {
const subtotal = math.multiply(
math.bignumber(price),
math.bignumber(quantity)
);
const tax = math.multiply(subtotal, math.bignumber(taxRate));
const total = math.add(subtotal, tax);
return math.format(total, { precision: 14 });
}
// GOOD: math.js for financial calculations
function calculateROI(
initialInvestment: number,
finalValue: number
): string {
const initial = math.bignumber(initialInvestment);
const final = math.bignumber(finalValue);
const difference = math.subtract(final, initial);
const ratio = math.divide(difference, initial);
const percentage = math.multiply(ratio, 100);
return math.format(percentage, { precision: 14 });
}
// BAD: JavaScript number for currency
function calculateTotal(price: number, quantity: number, taxRate: number): number {
const subtotal = price * quantity; // NO: precision errors
const tax = subtotal * taxRate; // NO: compounding errors
return subtotal + tax; // NO: wrong for money
}
// BAD: JavaScript number for percentages in finance
function calculateDiscount(price: number, discountPercent: number): number {
return price * (discountPercent / 100); // NO: precision errors
}
Why math.js:
number uses IEEE 754 double-precision floating-point0.1 + 0.2 !== 0.3When JavaScript number is OK:
Use null for absent values. undefined means uninitialized. Proactively coalesce to null.
// GOOD: null for absent, undefined for uninitialized
type User = {
name: string;
email: string;
phone: string | null; // may be absent
};
function findUser(id: string): User | null {
const user = database.users.get(id);
return user ?? null; // coalesce undefined to null
}
// GOOD: optional properties use ?:
type UserOptions = {
name: string;
email: string;
newsletter?: boolean; // may be undefined
};
// BAD: undefined for absent values
function findUser(id: string): User | undefined {
// prefer null for explicit absence
}
// GOOD: coalescing array access
const arr: Array<number> = [1, 2, 3];
const value: number | null = arr[10] ?? null;
Avoid enums. Use string literal unions instead.
// GOOD: string literal union
type Status = 'pending' | 'active' | 'complete' | 'failed';
function processStatus(status: Status): void {
switch (status) {
case 'pending':
// handle pending
break;
case 'active':
// handle active
break;
case 'complete':
// handle complete
break;
case 'failed':
// handle failed
break;
}
}
// BAD: enum
enum Status {
Pending = 'pending',
Active = 'active',
Complete = 'complete',
Failed = 'failed',
}
Rationale: String literal unions are simpler, work better with discriminated unions, and don't generate runtime code.
Always use unknown for truly unknown data. If a library forces any, escalate to operator for replacement.
// GOOD: unknown with type guard
function parseJSON(json: string): unknown {
return JSON.parse(json);
}
function processData(json: string): User {
const data: unknown = parseJSON(json);
if (isUser(data)) {
return data;
}
throw new Error('Invalid user data');
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
'email' in value
);
}
// BAD: using any
function parseJSON(json: string): any {
return JSON.parse(json);
}
Only for TypeScript system limitations. Always include comment explaining why.
// OK: DOM API limitation
const input = document.getElementById('email') as HTMLInputElement;
// DOM API returns HTMLElement, but we know it's an input
// OK: after runtime validation
const data: unknown = JSON.parse(jsonString);
if (isUser(data)) {
const user = data; // type guard narrows to User
}
// BAD: assertion without validation
const user = data as User; // no runtime check
// BAD: assertion to avoid type error
const value = (someValue as any) as TargetType;
Same rules as type assertions - sparingly, with justification.
// OK: after explicit check
const user = users.find(u => u.id === targetId);
if (user) {
processUser(user); // user is non-null here, no need for !
}
// OK (with comment): known initialization pattern
class Service {
private connection!: Connection;
// connection initialized in async init() called by constructor
constructor() {
this.init();
}
private async init(): Promise<void> {
this.connection = await createConnection();
}
}
// BAD: hiding real potential null
const value = map.get(key)!; // what if key doesn't exist?
Use type guards to narrow unknown types. Prefer built-in checks when possible.
// GOOD: typeof/instanceof for primitives/classes
function processValue(value: unknown): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
if (typeof value === 'number') {
return value.toString();
}
throw new Error('Unsupported type');
}
// GOOD: custom type guard with 'is'
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
typeof (value as any).name === 'string' &&
'email' in value &&
typeof (value as any).email === 'string'
);
}
// GOOD: discriminated union
type Result =
| {type: 'success'; data: string}
| {type: 'error'; message: string};
function handleResult(result: Result): void {
if (result.type === 'success') {
console.log(result.data); // narrowed to success
} else {
console.error(result.message); // narrowed to error
}
}
// GOOD: schema validation (TypeBox preferred)
import {Type, Static} from '@sinclair/typebox';
const UserSchema = Type.Object({
name: Type.String(),
email: Type.String(),
age: Type.Number(),
});
type User = Static<typeof UserSchema>;
function validateUser(data: unknown): data is User {
return Value.Che
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
npx skills add ed3dai/howto-code-in-typescript下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer