From typescript-clean-code-cookbook
Use this skill when writing, reviewing, or refactoring TypeScript code to ensure it follows clean code principles. Apply these standards to maintain clear, consistent, and maintainable code. Invoke automatically when generating functions, classes, or modules.
How this skill is triggered — by the user, by Claude, or both
Slash command
/typescript-clean-code-cookbook:typescript-clean-code-cookbookThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill provides guidance on writing clean, maintainable TypeScript code. Apply these principles when writing new code, refactoring existing code, or reviewing code quality.
This skill provides guidance on writing clean, maintainable TypeScript code. Apply these principles when writing new code, refactoring existing code, or reviewing code quality.
Apply clear, descriptive names that reveal intent:
id, tx, cb, errException: Do not apply naming rules to names required by interfaces, inheritance hierarchies, or third-party APIs.
Good:
function calculateMonthlyPayment(principal: number, interestRate: number): number {
return principal * interestRate / 12;
}
const userRepository = new UserRepository();
const users = userRepository.findActive();
Bad:
function calc(p: number, r: number): number { // Unclear abbreviations
return p * r / 12;
}
const repo = new UserRepository();
const u = repo.getActiveUsers(); // 'u' is not descriptive
Good (concise when parameters reveal intent):
arguments.get(name); // Not arguments.getArgument(name)
users.find(id); // Not users.findUser(id)
Use explicit types and avoid any:
any except as an absolute last resortany disables type checking and defeats the purpose of using TypeScriptunknown when the type is genuinely unknown - it forces you to narrow before useException: any may be acceptable when integrating with poorly-typed third-party libraries, but immediately wrap it with properly typed code.
Prefer explicit type definitions over inline typing:
Good (explicit type definitions):
interface CreateUserParams {
name: string;
email: string;
age: number;
preferences: {
newsletter: boolean;
notifications: boolean;
};
}
function createUser(params: CreateUserParams): User {
// Implementation
}
// Type is reusable and testable
const userData: CreateUserParams = {
name: 'John',
email: '[email protected]',
age: 30,
preferences: {
newsletter: true,
notifications: false
}
};
Bad (inline typing makes signature unwieldy):
function createUser(params: {
name: string;
email: string;
age: number;
preferences: {
newsletter: boolean;
notifications: boolean;
};
}): User {
// Implementation
}
// Cannot reuse the type, harder to test
Good (inline typing for simple cases):
function calculateTotal(items: { price: number }[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
function greet(user: { name: string }): string {
return `Hello, ${user.name}`;
}
Good (explicit types):
interface User {
id: string;
name: string;
email: string;
age: number;
}
function createUser(data: User): User {
return { ...data };
}
function processData(value: unknown): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
throw new Error('Expected string');
}
Bad (using any):
function createUser(data: any): any { // Lost all type safety
return { ...data };
}
function processData(value: any): any { // No type checking
return value.toUpperCase(); // Runtime error if value isn't string
}
Good (generic types maintain safety):
function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
const user = findById(users, '123'); // Type is User | undefined
const order = findById(orders, '456'); // Type is Order | undefined
Bad (any loses type information):
function findById(items: any[], id: string): any {
return items.find(item => item.id === id);
}
const user = findById(users, '123'); // Type is any - no safety
const order = findById(orders, '456'); // Type is any - no safety
Good (wrapping third-party any):
import { poorlyTypedLibrary } from 'some-library';
interface LibraryResponse {
data: string;
status: number;
}
function callLibrary(input: string): LibraryResponse {
const result: any = poorlyTypedLibrary.call(input); // Last resort any
// Immediately convert to proper types
return {
data: String(result.data),
status: Number(result.status)
};
}
Write small, focused functions that do one thing well:
Single Level of Abstraction:
Counting meaningful lines (for the 8-line guideline):
Exception: Do not apply function rules to third-party library functions.
Good (small, single purpose, verb name):
function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function sendWelcomeEmail(user: User): void {
const template = getWelcomeTemplate();
const message = populateTemplate(template, user);
emailService.send(user.email, message);
}
Bad (too large, multiple responsibilities):
function processUser(user: User, sendEmail: boolean): void { // Boolean parameter
// Validation
if (!user.email) throw new Error('No email');
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(user.email)) throw new Error('Invalid email');
// Save to database
const connection = createConnection();
connection.query('INSERT INTO users...');
// Send email conditionally
if (sendEmail) { // Conditional logic from boolean param
const template = fs.readFileSync('welcome.html');
const message = template.replace('{{name}}', user.name);
smtp.send(user.email, message);
}
}
Good (single level of abstraction - high level):
function processOrder(order: Order): void {
validateOrder(order);
chargePayment(order);
shipProducts(order);
sendConfirmation(order);
}
Bad (mixed abstraction levels):
function processOrder(order: Order): void {
// High-level: validation
validateOrder(order);
// Low-level: payment processing details mixed in
const stripe = new Stripe(apiKey);
const charge = await stripe.charges.create({
amount: order.total * 100,
currency: 'usd',
source: order.token
});
// High-level: shipping
shipProducts(order);
}
Return promises directly - don't use return await:
await when you need the value for further processingException: Use return await when inside a try-catch block and you need to catch errors from the promise.
Good (return promise directly):
async function getUser(id: string): Promise<User> {
return userRepository.findById(id);
}
async function processOrder(orderId: string): Promise<void> {
return orderService.process(orderId);
}
Bad (unnecessary await):
async function getUser(id: string): Promise<User> {
return await userRepository.findById(id); // Redundant await
}
async function processOrder(orderId: string): Promise<void> {
return await orderService.process(orderId); // Redundant await
}
Good (await when you need the value):
async function getUserWithOrders(id: string): Promise<UserWithOrders> {
const user = await userRepository.findById(id); // Need user object
const orders = await orderRepository.findByUserId(user.id); // Use user.id
return { user, orders };
}
Exception - use await in try-catch:
async function getUser(id: string): Promise<User> {
try {
return await userRepository.findById(id); // Needed to catch errors here
} catch (error) {
logger.error('Failed to fetch user', { id, error });
throw error;
}
}
Eliminate conditionals through design:
else blocks and nested conditionalsMove decisions upstream:
Exception: Do not apply these rules to code using the Factory pattern.
Good (guard clauses):
function calculateDiscount(user: User, amount: number): number {
if (!user.isPremium) return 0;
if (amount < 100) return 0;
return amount * 0.1;
}
Bad (nested conditionals):
function calculateDiscount(user: User, amount: number): number {
if (user.isPremium) {
if (amount >= 100) {
return amount * 0.1;
} else {
return 0;
}
} else {
return 0;
}
}
Good (polymorphism instead of conditionals):
interface PaymentProcessor {
process(amount: number): void;
}
class CreditCardProcessor implements PaymentProcessor {
process(amount: number): void {
// Credit card specific logic
}
}
class PayPalProcessor implements PaymentProcessor {
process(amount: number): void {
// PayPal specific logic
}
}
function checkout(processor: PaymentProcessor, amount: number): void {
processor.process(amount); // No conditionals
}
Bad (conditional logic):
function checkout(paymentType: string, amount: number): void {
if (paymentType === 'credit-card') {
// Credit card logic
} else if (paymentType === 'paypal') {
// PayPal logic
} else if (paymentType === 'bitcoin') {
// Bitcoin logic
}
}
Good (separate functions instead of boolean parameter):
function activateUser(user: User): void {
user.status = 'active';
sendActivationEmail(user);
}
function deactivateUser(user: User): void {
user.status = 'inactive';
sendDeactivationEmail(user);
}
Bad (boolean parameter creates conditional):
function setUserStatus(user: User, activate: boolean): void {
if (activate) {
user.status = 'active';
sendActivationEmail(user);
} else {
user.status = 'inactive';
sendDeactivationEmail(user);
}
}
Comments should explain why, never what:
Valid reasons for comments:
Good:
// Using setTimeout instead of setInterval because the API rate limit
// requires we wait for each response before making the next request
function pollApi(): void {
fetchData().then(() => {
setTimeout(pollApi, 1000);
});
}
// Workaround for bug in library v2.3.1 - see issue #1234
const result = workaroundFunction();
Bad:
// Loop through users
users.forEach(user => {
// Check if user is active
if (user.isActive) {
// Send email
sendEmail(user);
}
});
// Increment counter
counter++;
Better (no comments needed):
activeUsers.forEach(sendEmail);
counter++;
Blank lines should not be used for visual separation within functions:
In production code:
Bad (blank lines separating logical blocks in route handler):
app.get('/users', (req, res) => {
const limit = parseInt(req.query.limit as string) || 10;
const users = userService.getUsers(limit);
res.json(users);
});
Good (no blank lines needed for simple flow):
app.get('/users', (req, res) => {
const limit = parseInt(req.query.limit as string) || 10;
const users = userService.getUsers(limit);
res.json(users);
});
Bad (blank lines suggest missing function extraction):
function processOrder(order: Order): void {
const total = order.items.reduce((sum, item) => sum + item.price, 0);
const tax = total * 0.1;
const finalAmount = total + tax;
const payment = stripe.charges.create({ amount: finalAmount });
order.paymentId = payment.id;
emailService.send(order.userEmail, generateReceipt(order));
logger.info('Order processed', { orderId: order.id });
}
Good (extract logical blocks into functions):
function processOrder(order: Order): void {
const finalAmount = calculateOrderTotal(order);
const paymentId = processPayment(finalAmount);
sendOrderConfirmation(order, paymentId);
}
In unit tests:
Bad (unnecessary blank lines for trivial single-line sections):
it('calculates total correctly', () => {
const cart = new ShoppingCart();
cart.addItem({ price: 100 });
eq(cart.getTotal(), 100);
});
Good (no blank lines needed for simple test):
it('calculates total correctly', () => {
const cart = new ShoppingCart();
cart.addItem({ price: 100 });
eq(cart.getTotal(), 100);
});
Good (blank lines appropriate for multi-line setup and assertions):
it('applies discount to premium users with large orders', () => {
const cart = new ShoppingCart();
cart.addItem({ price: 100 });
cart.addItem({ price: 50 });
const user = { isPremium: true };
const discount = cart.calculateDiscount(user);
eq(discount, 15);
eq(cart.getTotal(), 135);
eq(cart.items.length, 2);
ok(user.isPremium);
eq(cart.discountApplied, true);
});
Bad (multi-line sections without separation are hard to parse):
it('applies discount to premium users with large orders', () => {
const cart = new ShoppingCart();
cart.addItem({ price: 100 });
cart.addItem({ price: 50 });
const user = { isPremium: true };
const discount = cart.calculateDiscount(user);
eq(discount, 15);
eq(cart.getTotal(), 135);
eq(cart.items.length, 2);
ok(user.isPremium);
eq(cart.discountApplied, true);
});
Practice proper encapsulation:
Avoid helpers and utils:
Exception: Do not apply these rules to code whose sole purpose is to act as a conduit for configuration or request data (e.g., DTOs, configuration objects).
Good (behaviour, not data exposure):
class ShoppingCart {
private items: Item[] = [];
addItem(item: Item): void {
this.items.push(item);
}
calculateTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
applyDiscount(percentage: number): void {
this.items.forEach(item => {
item.price *= (1 - percentage / 100);
});
}
}
Bad (exposed data, separate helper):
class ShoppingCart {
public items: Item[] = []; // Exposed internal data
getItems(): Item[] { // Unnecessary getter
return this.items;
}
setItems(items: Item[]): void { // Unnecessary setter
this.items = items;
}
}
// Helper function indicates missing domain concept
function calculateCartTotal(cart: ShoppingCart): number {
return cart.getItems().reduce((sum, item) => sum + item.price, 0);
}
// Another helper - behaviour should be in the cart
function applyDiscountToCart(cart: ShoppingCart, percentage: number): void {
cart.getItems().forEach(item => {
item.price *= (1 - percentage / 100);
});
}
Use object parameters when dependencies have no natural order:
When to use object parameters:
When positional parameters are acceptable:
Point(x, y)) that are unlikely to growArray.slice(start, end))Good (object parameter with explicit type):
interface UserServiceParams {
config: Config;
database: Database;
logger: Logger;
emailService: EmailService;
}
class UserService {
private config: Config;
private database: Database;
private logger: Logger;
private emailService: EmailService;
constructor({ config, database, logger, emailService }: UserServiceParams) {
this.config = config;
this.database = database;
this.logger = logger;
this.emailService = emailService;
}
}
// Call site is clear and order-independent
const userService = new UserService({
config,
database,
logger,
emailService
});
Bad (positional parameters with no natural order):
class UserService {
private config: Config;
private database: Database;
private logger: Logger;
private emailService: EmailService;
constructor(
config: Config,
database: Database,
logger: Logger,
emailService: EmailService
) {
this.config = config;
this.database = database;
this.logger = logger;
this.emailService = emailService;
}
}
// Call site requires remembering arbitrary order
const userService = new UserService(
config,
database,
logger,
emailService
);
// Easy to accidentally swap parameters
const userService = new UserService(
config,
logger, // Wrong order!
database, // Wrong order!
emailService
);
Good (positional parameters for simple value object):
class Point {
constructor(
private x: number,
private y: number
) {}
}
const point = new Point(10, 20); // x, y is natural and conventional
Good (object parameter for consistency and future growth):
interface PaymentServiceParams {
config: Config;
}
class PaymentService {
private config: Config;
constructor({ config }: PaymentServiceParams) {
this.config = config;
}
}
// Consistent with other services, easy to add dependencies later
const paymentService = new PaymentService({ config });
// Later, adding a logger doesn't break existing call sites
interface PaymentServiceParams {
config: Config;
logger?: Logger; // New optional parameter
}
Fail fast and fail clearly:
Good:
function processPayment(amount: number): void {
if (amount <= 0) {
throw new Error(`Invalid payment amount: ${amount}. Amount must be positive.`);
}
if (!isConnectedToPaymentGateway()) {
throw new Error('Cannot process payment: payment gateway unavailable');
}
// Process payment
}
Bad:
function processPayment(amount: number): number {
if (amount <= 0) {
return -1; // Return code instead of exception
}
if (!isConnectedToPaymentGateway()) {
console.log('Warning: gateway unavailable');
return 0; // Hiding error, continuing with corrupted state
}
// Process payment
return 1; // Success code
}
Eliminate duplication ruthlessly:
Good:
function validateUserInput(input: string, fieldName: string): void {
if (!input || input.trim().length === 0) {
throw new Error(`${fieldName} is required`);
}
}
function createUser(name: string, email: string, password: string): User {
validateUserInput(name, 'Name');
validateUserInput(email, 'Email');
validateUserInput(password, 'Password');
return new User(name, email, password);
}
Bad:
function createUser(name: string, email: string, password: string): User {
// Repeated validation logic
if (!name || name.trim().length === 0) {
throw new Error('Name is required');
}
if (!email || email.trim().length === 0) {
throw new Error('Email is required');
}
if (!password || password.trim().length === 0) {
throw new Error('Password is required');
}
return new User(name, email, password);
}
npx claudepluginhub cressie176/cressie176-claude-marketplace --plugin typescript-clean-code-cookbookEnforces TypeScript conventions for strict, type-safe code: no `any` use unknown, interfaces vs types, literal unions over enums, discriminated unions, type narrowing with guards, branded types, and anti-pattern avoidance.
Provides TypeScript best practices for type-safe code including strict mode, interfaces, discriminated unions, generics, async patterns, and null safety. Useful for type definitions and maintainable TS.
TypeScript type system, strict mode, and TS-specific patterns beyond JavaScript fundamentals. Invoke whenever task involves any interaction with TypeScript code — writing, reviewing, refactoring, debugging .ts/.tsx files, type definitions, generics, narrowing, tsconfig, or type-level programming.