Marketplace typescript-node-expert
Expert TypeScript/Node.js developer for building high-quality, performant, and maintainable CLI tools and libraries. Enforces best practices, strict typing, and modern patterns.
install
source · Clone the upstream repo
git clone https://github.com/aiskillstore/marketplace
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aiskillstore/marketplace "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/dammianmiller/typescript-node-expert" ~/.claude/skills/aiskillstore-marketplace-typescript-node-expert && rm -rf "$T"
manifest:
skills/dammianmiller/typescript-node-expert/SKILL.mdsource content
TypeScript/Node.js Expert
Overview
This skill provides expert guidance for TypeScript and Node.js development with a focus on:
- Type Safety: Strict TypeScript with full type coverage
- Performance: Async patterns, streaming, memory efficiency
- Maintainability: Clean architecture, SOLID principles
- Modern Standards: ES2022+, ESM modules, latest Node.js features
PROACTIVE USAGE
Invoke this skill before ANY TypeScript/Node.js work:
- New features or modules
- Refactoring existing code
- Performance optimization
- Code review
- Bug fixes in TypeScript files
Critical Rules - Zero Tolerance
1. Strict TypeScript Configuration
Required
settings:tsconfig.json
{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, "moduleResolution": "NodeNext", "module": "NodeNext", "target": "ES2022", "declaration": true, "declarationMap": true, "sourceMap": true } }
2. No any
- Ever
any// ❌ FORBIDDEN function process(data: any) { ... } const result: any = await fetch(); // ✅ REQUIRED function process(data: unknown) { ... } function process<T extends Record<string, unknown>>(data: T) { ... } // Use type guards function isValidResponse(data: unknown): data is ApiResponse { return typeof data === 'object' && data !== null && 'status' in data; }
3. Explicit Return Types
// ❌ FORBIDDEN async function getData() { return await db.query(); } // ✅ REQUIRED async function getData(): Promise<User[]> { return await db.query(); }
4. Null Safety
// ❌ FORBIDDEN const name = user.profile.name; // Could be undefined // ✅ REQUIRED - Optional chaining + nullish coalescing const name = user?.profile?.name ?? 'Unknown'; // ✅ REQUIRED - Early return pattern if (!user?.profile?.name) { throw new Error('User profile name is required'); } const name = user.profile.name;
Performance Patterns
1. Async/Await Best Practices
// ❌ SLOW - Sequential const user = await getUser(id); const posts = await getPosts(id); const comments = await getComments(id); // ✅ FAST - Parallel const [user, posts, comments] = await Promise.all([ getUser(id), getPosts(id), getComments(id), ]); // ✅ CONTROLLED - Promise.allSettled for fault tolerance const results = await Promise.allSettled([ fetchFromService1(), fetchFromService2(), fetchFromService3(), ]); const successful = results .filter((r): r is PromiseFulfilledResult<Data> => r.status === 'fulfilled') .map(r => r.value);
2. Streaming for Large Data
import { createReadStream, createWriteStream } from 'fs'; import { pipeline } from 'stream/promises'; import { Transform } from 'stream'; // ❌ BAD - Loads entire file into memory const content = await fs.readFile('large-file.json', 'utf-8'); const data = JSON.parse(content); // ✅ GOOD - Stream processing async function processLargeFile(inputPath: string, outputPath: string): Promise<void> { const transform = new Transform({ objectMode: true, transform(chunk, encoding, callback) { const processed = processChunk(chunk); callback(null, processed); }, }); await pipeline( createReadStream(inputPath), transform, createWriteStream(outputPath) ); }
3. Memory-Efficient Collections
// ❌ BAD - Creates intermediate arrays const result = data .filter(x => x.active) .map(x => x.id) .slice(0, 10); // ✅ GOOD - Generator for lazy evaluation function* filterAndMap<T, U>( items: Iterable<T>, predicate: (item: T) => boolean, mapper: (item: T) => U, limit = Infinity ): Generator<U> { let count = 0; for (const item of items) { if (count >= limit) return; if (predicate(item)) { yield mapper(item); count++; } } } const result = [...filterAndMap(data, x => x.active, x => x.id, 10)];
Error Handling
1. Custom Error Classes
// Define error hierarchy export class AppError extends Error { constructor( message: string, public readonly code: string, public readonly statusCode: number = 500, public readonly isOperational: boolean = true ) { super(message); this.name = this.constructor.name; Error.captureStackTrace(this, this.constructor); } } export class ValidationError extends AppError { constructor(message: string, public readonly field?: string) { super(message, 'VALIDATION_ERROR', 400); } } export class NotFoundError extends AppError { constructor(resource: string, id: string) { super(`${resource} with id ${id} not found`, 'NOT_FOUND', 404); } }
2. Result Pattern (No Throw for Expected Failures)
type Result<T, E = Error> = | { success: true; data: T } | { success: false; error: E }; async function parseConfig(path: string): Promise<Result<Config, string>> { try { const content = await fs.readFile(path, 'utf-8'); const config = JSON.parse(content); if (!isValidConfig(config)) { return { success: false, error: 'Invalid configuration format' }; } return { success: true, data: config }; } catch (e) { return { success: false, error: `Failed to read config: ${e}` }; } } // Usage const result = await parseConfig('.config.json'); if (!result.success) { console.error(result.error); process.exit(1); } console.log(result.data);
CLI Development Patterns
1. Commander.js Structure
import { Command, Option } from 'commander'; const program = new Command() .name('my-cli') .description('CLI description') .version('1.0.0', '-v, --version'); // Subcommand with options program .command('generate') .description('Generate output files') .argument('<input>', 'Input file path') .option('-o, --output <path>', 'Output path', './output') .option('-f, --format <type>', 'Output format', 'json') .option('--dry-run', 'Preview without writing', false) .addOption( new Option('-l, --log-level <level>', 'Log level') .choices(['debug', 'info', 'warn', 'error']) .default('info') ) .action(async (input: string, options: GenerateOptions) => { try { await runGenerate(input, options); } catch (error) { console.error(chalk.red(`Error: ${error instanceof Error ? error.message : error}`)); process.exit(1); } }); program.parseAsync();
2. User Feedback
import ora from 'ora'; import chalk from 'chalk'; async function runWithSpinner<T>( message: string, task: () => Promise<T> ): Promise<T> { const spinner = ora(message).start(); try { const result = await task(); spinner.succeed(); return result; } catch (error) { spinner.fail(); throw error; } } // Progress for multi-step operations async function processFiles(files: string[]): Promise<void> { const total = files.length; for (let i = 0; i < files.length; i++) { const file = files[i]!; process.stdout.write(`\r${chalk.cyan('Processing')} [${i + 1}/${total}] ${file}`); await processFile(file); } console.log(chalk.green('\n✔ All files processed')); }
Module Organization
1. Barrel Exports
// types/index.ts - Export all types export type { Config, Options, Result } from './config.js'; export type { User, UserProfile } from './user.js'; // Use in imports import type { Config, User } from './types/index.js';
2. Dependency Injection
// Define interfaces interface Logger { info(message: string): void; error(message: string, error?: Error): void; } interface Database { query<T>(sql: string, params?: unknown[]): Promise<T[]>; } // Service with injected dependencies class UserService { constructor( private readonly db: Database, private readonly logger: Logger ) {} async getUser(id: string): Promise<User | null> { this.logger.info(`Fetching user ${id}`); const [user] = await this.db.query<User>('SELECT * FROM users WHERE id = ?', [id]); return user ?? null; } }
Testing Standards
1. Vitest Configuration
// vitest.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, environment: 'node', coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], thresholds: { lines: 80, functions: 80, branches: 75, statements: 80, }, }, include: ['src/**/*.test.ts'], typecheck: { enabled: true, include: ['src/**/*.test.ts'], }, }, });
2. Test Patterns
import { describe, it, expect, vi, beforeEach } from 'vitest'; describe('UserService', () => { let service: UserService; let mockDb: Database; let mockLogger: Logger; beforeEach(() => { mockDb = { query: vi.fn(), }; mockLogger = { info: vi.fn(), error: vi.fn(), }; service = new UserService(mockDb, mockLogger); }); it('should return user when found', async () => { const expectedUser: User = { id: '1', name: 'Test' }; vi.mocked(mockDb.query).mockResolvedValue([expectedUser]); const result = await service.getUser('1'); expect(result).toEqual(expectedUser); expect(mockLogger.info).toHaveBeenCalledWith('Fetching user 1'); }); it('should return null when user not found', async () => { vi.mocked(mockDb.query).mockResolvedValue([]); const result = await service.getUser('999'); expect(result).toBeNull(); }); });
Code Review Checklist
Before completing ANY TypeScript work:
-
in tsconfig.jsonstrict: true - No
types (useany
+ type guards)unknown - All functions have explicit return types
- Errors use custom error classes or Result pattern
- Async operations use Promise.all where possible
- Large data uses streaming
- All exports have JSDoc comments
- Tests cover happy path, edge cases, and error cases
-
passesnpm run lint -
passesnpm run test - No console.log (use proper logger)
Quick Reference
// Type assertions (prefer type guards) const data = value as Data; // ❌ Avoid if (isData(value)) { ... } // ✅ Prefer // Object destructuring with defaults const { name = 'default', age } = user; // Array methods with type narrowing const numbers = mixed.filter((x): x is number => typeof x === 'number'); // Readonly for immutability function process(items: readonly Item[]): void { ... } // Template literal types type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; type Endpoint = `/${string}`; type Route = `${HttpMethod} ${Endpoint}`;