| name | typescript-guidelines |
| description | TypeScript coding guidelines including types, imports, exports, and patterns. Auto-loaded when working with TypeScript files. |
| category | guideline |
| user-invocable | false |
TypeScript Guidelines
TypeScript Version
- TypeScript 5.x+
- Strict mode enabled
Type Usage Rules
Avoid any Type
Use specific types or unknown instead of any:
function processData(data: any) {
return data.value;
}
function processData(data: { value: string }) {
return data.value;
}
function processData(data: unknown) {
if (typeof data === 'object' && data !== null && 'value' in data) {
return (data as { value: string }).value;
}
}
Use satisfies Instead of as
Prefer satisfies for type assertions:
const config = {
url: '/api/data',
method: 'GET',
} satisfies RequestConfig;
const config = {
url: '/api/data',
method: 'GET',
} as RequestConfig;
When as is acceptable:
- Narrowing after type guards
- DOM element type assertions
- Event target casting
const target = event.target as HTMLInputElement;
const element = document.querySelector('.button') as HTMLButtonElement | null;
Type Inference
Let TypeScript infer types when possible:
const count = 0;
const name = 'test';
const items = [1, 2, 3];
const count: number = 0;
const name: string = 'test';
When to annotate explicitly:
- Function parameters (always)
- Function return types (when complex or public API)
- Variables when inference would be too broad
function calculateValue(input: number, offset: number): number {
return input + offset;
}
const items: string[] = [];
Functional Programming Patterns
Prefer Array Methods Over Loops
Use array methods instead of for loops:
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);
const usersById = users.reduce((acc, user) => {
acc[user.id] = user;
return acc;
}, {} as Record<string, User>);
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
When loops are acceptable:
- Performance-critical hot paths (measure first)
- Breaking early from iteration
- Complex state machines
Immutability
Avoid mutation, create new objects:
const newArray = [...oldArray, newItem];
const newObject = { ...oldObject, updatedField: newValue };
const filtered = items.filter(item => item.active);
oldArray.push(newItem);
oldObject.updatedField = newValue;
Imports & Exports
Import Organization
Order and spacing:
import { something } from 'external-package';
import { utility } from '@project/utils';
import { helper } from './utils/helper';
import { Type } from './types';
const myFunction = () => {};
Named Exports Only
Use named exports consistently:
export function calculateValue(value: number) {
return value * 2;
}
export interface Config {
url: string;
}
export default function calculateValue(value: number) {
return value * 2;
}
Benefits:
- Named exports enable better IDE refactoring
- Enforces consistent naming across imports
- Prevents confusion from different import names
Variable Declarations
Use const and let Only
const MAX_VALUE = 10;
let currentValue = 0;
var MAX_VALUE = 10;
Prefer const Over let
const result = calculate(id);
const displayName = result?.name ?? 'Unknown';
let result = calculate(id);
Type Definitions
Interface vs Type
Prefer interface for object shapes:
export interface User {
id: string;
name: string;
email: string;
}
export type UserId = string;
export type Status = 'active' | 'inactive' | 'error';
export type PartialUser = Partial<User>;
Generic Types
function selectById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
const user = selectById(users, 'user-1');
Enums vs Union Types
Prefer union types over enums:
type Status = 'pending' | 'success' | 'error';
function getStatus(): Status {
return 'success';
}
enum Status {
Pending = 'pending',
Success = 'success',
Error = 'error',
}
Rationale: Union types are more lightweight and work better with TypeScript's type system.
Nullable Values
Optional vs Undefined vs Null
Prefer undefined over null:
function findUser(id: string): User | undefined {
return users.find(u => u.id === id);
}
function findUser(id: string): User | null {
return users.find(u => u.id === id) ?? null;
}
Nullish Coalescing
const timeout = config.timeout ?? 5000;
const name = user?.name ?? 'Unknown';
const timeout = config.timeout || 5000;
Type Guards
Custom Type Guards
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
typeof (value as User).id === 'string' &&
typeof (value as User).name === 'string'
);
}
if (isUser(data)) {
console.log(data.name);
}
Utility Types
Common Utility Types
type PartialUser = Partial<User>;
type RequiredUser = Required<PartialUser>;
type UserName = Pick<User, 'id' | 'name'>;
type UserWithoutMeta = Omit<User, 'created_at' | 'updated_at'>;
type UserMap = Record<string, User>;
type UserReturn = ReturnType<typeof getUser>;
Error Handling
Error Types
try {
await fetchData();
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error('Unknown error', error);
}
}
Known Gotchas
any Type
Avoid any at all costs - it disables type checking completely. Use unknown for truly unknown types, then narrow with type guards.
Type Assertion with as
Only use as for DOM elements and after type guards. Prefer satisfies for configuration objects.
Enum Runtime Overhead
Enums generate runtime code. Prefer union types which are compile-time only.
Default Exports
Never use default exports. They prevent consistent naming and break IDE refactoring.
Error Type in Catch Blocks
Errors in catch blocks are always unknown. Use type guards to narrow.