.config/opencode/skills/typescript-best-practices/SKILL.md
Guides TypeScript best practices for type safety, code organization, and maintainability. Use this skill when configuring TypeScript projects, deciding on typing strategies, writing async code, or reviewing TypeScript code quality.
npx skillsauth add klen/dotfiles typescript-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Comprehensive guide to writing clean, type-safe, and maintainable TypeScript code.
Always enable strict mode for maximum type safety:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
Why strict mode matters:
any types from sneaking inLet TypeScript infer types when obvious:
// Good - inference works fine
const name = 'Alice';
const count = 42;
const items = ['a', 'b', 'c'];
// Bad - redundant annotations
const name: string = 'Alice';
const count: number = 42;
// Good - explicit for function signatures
function calculateTotal(items: CartItem[], taxRate: number): number {
return items.reduce((sum, item) => sum + item.price, 0) * (1 + taxRate);
}
// Good - explicit for class properties
class UserService {
private readonly cache: Map<string, User>;
constructor(private api: ApiClient) {
this.cache = new Map();
}
}
Use interface for:
interface User {
id: string;
name: string;
}
interface Admin extends User {
permissions: string[];
}
Use type for:
type Status = 'pending' | 'approved' | 'rejected';
type Point = [number, number];
type ReadonlyUser = Readonly<User>;
any - Use unknown with Type Guards// Bad - defeats type checking
function process(data: any) {
return data.toUpperCase(); // No error, but might crash
}
// Good - use unknown with type guards
function process(data: unknown): string {
if (typeof data === 'string') {
return data.toUpperCase();
}
throw new Error('Expected string');
}
// Good - use generics for flexibility
function identity<T>(value: T): T {
return value;
}
Use lowercase with dots for clarity:
src/
├── user/
│ ├── user.service.ts
│ ├── user.model.ts
│ ├── user.controller.ts
│ └── index.ts # Barrel file
├── auth/
│ ├── auth.service.ts
│ └── index.ts
└── types/
└── index.ts
// user/index.ts
export { UserService } from './user.service';
export { User, CreateUserDto } from './user.model';
export { UserController } from './user.controller';
// Consumer imports cleanly
import { UserService, User } from './user';
// Good - clear contract
function greet(name: string, greeting = 'Hello'): string {
return `${greeting}, ${name}!`;
}
// Good - use rest parameters for variable arguments
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
// Bad - function does too much
function processUser(user: User) {
// validates, transforms, saves, and sends email
}
// Good - split into focused functions
function validateUser(user: User): ValidationResult { ... }
function transformUser(user: User): TransformedUser { ... }
function saveUser(user: TransformedUser): Promise<void> { ... }
function sendWelcomeEmail(user: User): Promise<void> { ... }
// Good - guard clauses
function processOrder(order: Order | null): ProcessedOrder {
if (!order) throw new Error('Order required');
if (order.items.length === 0) throw new Error('Order must have items');
if (order.status !== 'pending') throw new Error('Order already processed');
// Main logic here - no nesting
return { ...order, status: 'processed' };
}
// Good - explicit error handling
async function fetchUser(id: string): Promise<User> {
try {
const response = await api.get(`/users/${id}`);
return response.data;
} catch (error) {
if (error instanceof NotFoundError) {
throw new UserNotFoundError(id);
}
throw error;
}
}
// Bad - sequential when parallel is possible
const user = await fetchUser(id);
const orders = await fetchOrders(id);
const preferences = await fetchPreferences(id);
// Good - parallel execution
const [user, orders, preferences] = await Promise.all([
fetchUser(id),
fetchOrders(id),
fetchPreferences(id),
]);
// Bad - callback hell with async
async function bad() {
return fetchUser().then(user => {
return fetchOrders(user.id).then(orders => {
return processOrders(orders).then(result => {
return result;
});
});
});
}
// Good - flat async/await
async function good() {
const user = await fetchUser();
const orders = await fetchOrders(user.id);
return processOrders(orders);
}
interface PaymentGateway {
charge(amount: number): Promise<boolean>;
}
class PaymentProcessor {
constructor(private gateway: PaymentGateway) {}
async processPayment(amount: number): Promise<boolean> {
if (amount <= 0) throw new Error('Amount must be positive');
return this.gateway.charge(amount);
}
}
// Easy to test with mock
const mockGateway: PaymentGateway = {
charge: jest.fn().mockResolvedValue(true),
};
const processor = new PaymentProcessor(mockGateway);
interface Cat {
meow(): void;
}
interface Dog {
bark(): void;
}
function isCat(pet: Cat | Dog): pet is Cat {
return 'meow' in pet;
}
function makeSound(pet: Cat | Dog) {
if (isCat(pet)) {
pet.meow(); // TypeScript knows it's Cat
} else {
pet.bark(); // TypeScript knows it's Dog
}
}
// Good - type stripped at compile time, better tree-shaking
import type { User, Order } from './types';
import { fetchUser } from './api';
// Also good for re-exports
export type { User, Order };
// Creates readonly tuple with literal types
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number]; // "red" | "green" | "blue"
// Works for objects too
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
} as const;
// Bad - deeply nested mapped types slow compilation
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
// Consider simpler alternatives or use sparingly
// Good - safe property access
function getLength(str: string | null): number {
return str?.length ?? 0;
}
// Good - safe method calls
const result = user?.getProfile?.()?.name ?? 'Anonymous';
// Good - default values only for null/undefined
const port = config.port ?? 3000; // 0 is valid, won't use default
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function renderState<T>(state: AsyncState<T>) {
switch (state.status) {
case 'idle':
return 'Ready';
case 'loading':
return 'Loading...';
case 'success':
return state.data; // TypeScript knows data exists
case 'error':
return state.error.message; // TypeScript knows error exists
}
}
| Mistake | Problem | Solution |
|---------|---------|----------|
| Overusing any | Defeats type checking | Use unknown, generics, or proper types |
| Not using strict mode | Misses many errors | Enable "strict": true |
| Redundant annotations | Clutters code | Trust type inference |
| Ignoring union types | Runtime errors | Use type guards |
| Not handling null | Crashes | Use ?. and ?? operators |
| Nested conditionals | Hard to read | Use guard clauses |
// Type inference - let TS do the work
const name = 'Alice';
// Explicit for APIs
function greet(name: string): string { ... }
// Unknown over any
function safe(data: unknown) { ... }
// Type-only imports
import type { User } from './types';
// Const assertions
const tuple = [1, 2] as const;
// Null safety
const len = str?.length ?? 0;
// Guard clauses
if (!valid) throw new Error();
// main logic...
tools
Anti-patterns and mistakes to avoid as a product manager. Use when evaluating leadership behaviors, improving team dynamics, reflecting on management practices, or onboarding new product managers.
development
Guides proper usage of TypeScript's satisfies operator vs type annotations. Use this skill when deciding between type annotations (colon) and satisfies, validating object shapes while preserving literal types, or troubleshooting type inference issues.
development
Guides when to use interface vs type in TypeScript. Use this skill when defining object types, extending types, or choosing between interface and type aliases.
tools
Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or ensuring compile-time type safety in TypeScript projects.