AgentSkillsCN

ts-coding-skill

提供TypeScript Node.js风格指南,并附带丰富示例,助力代码一致性与最佳实践的落实。在编写TypeScript Node.js代码时使用此功能。

SKILL.md
--- frontmatter
name: ts-coding-skill
description: TypeScript Node.js style guide with expanded examples for code consistency and best practices. Use when writing TypeScript Node.js code.
license: MIT
compatibility: opencode
metadata:
  audience: developers
  workflow: coding

TypeScript Node.js Coding Style

Follow these guidelines for writing clean, functional, and maintainable TypeScript Node.js code.

Package Management

Always use bun for package management. Never use npm, yarn, or pnpm.

bash
# Correct
bun add lodash
bun install
bun run dev

# Incorrect
npm install lodash
npm install
npm run dev

Research Before Coding

Unless the code fix is trivial (simple rename, obvious typo, etc.), research relevant libraries before writing code:

  1. Use context7 MCP tool for library documentation
  2. Use gh_grep MCP tool for real-world code examples
  3. Only use web search if the previous tools do not work
bash
# First, research the library
context7_query-docs(libraryId: '/colinhacks/zod', query: 'How to validate email addresses')

# Then, find real examples
gh_grep_searchGitHub(query: 'z.string().email()', language: ['TypeScript'])

Validation and Testing

Run Validation After Each Command

If the below commands are defined, run them for validation:

bash
# Run after each non-trivial code change
bun run typecheck && bun run check:fix

# This runs:
# - tsc --noEmit (type checking)
# - eslint --fix . (linting with auto-fix)
# - prettier --write . (formatting)

# If there are errors in other files being worked on by another agent, ignore those.
# Only fix errors in files you modified.

Run Targeted Unit Tests

bash
# Only run tests for the specific test file you're writing
bun test src/utils/user.test.ts

# Do not run the entire test suite
# Ignore all other test failures (don't try to fix them)

Review Code Changes

bash
# After making changes, review your work
git diff

# Verify the changes follow all "Coding Style" rules:
# - ✓ Uses functional primitives (.map, .filter, .flatMap)
# - ✓ Prefers const over let
# - ✓ Has explicit type annotations
# - ✓ Avoids type casting
# - ✓ Has docstrings on all classes/functions
# - ✓ Uses named parameters for 2+ args
# - ✓ Uses sibling absolute imports
# - ✓ Has try/catch only at root level
# - ✓ Uses dedent for multiline strings

Functional Coding Style

Prefer Array Methods Over Imperative Loops

typescript
// ✅ CORRECT - Use .map for transformations
const names = users.map(user => user.name);

// ❌ INCORRECT - Avoid imperative loops
const names: string[] = [];
for (const user of users) {
  names.push(user.name);
}

// ✅ CORRECT - Use .filter for filtering
const adults = users.filter(user => user.age >= 18);

// ❌ INCORRECT - Avoid imperative accumulation
const adults: User[] = [];
for (const user of users) {
  if (user.age >= 18) {
    adults.push(user);
  }
}

// ✅ CORRECT - Use .flatMap for flattening
const allTags = posts.flatMap(post => post.tags);

// ❌ INCORRECT - Avoid nested loops
const allTags: string[] = [];
for (const post of posts) {
  for (const tag of post.tags) {
    allTags.push(tag);
  }
}

Prefer Object.fromEntries for Object Creation

typescript
// ✅ CORRECT - Functional approach
const userMap = Object.fromEntries(
  users.map(user => [user.id, user])
);

// ❌ INCORRECT - Mutation-based approach
const userMap: Record<string, User> = {};
for (const user of users) {
  userMap[user.id] = user;
}

// ✅ CORRECT - Transform keys functionally
const nameMap = Object.fromEntries(
  Object.entries(userMap).map(([id, user]) => [user.name, id])
);

// ❌ INCORRECT - Mutative key transformation
const nameMap: Record<string, string> = {};
for (const [id, user] of Object.entries(userMap)) {
  nameMap[user.name] = id;
}

Prefer const Over let and Mutation

typescript
// ✅ CORRECT - Use const with anonymous function for complex logic
const total = (() => {
  let sum = 0;
  for (const item of items) {
    sum += item.price;
  }
  return sum;
})();

// ✅ CORRECT - Use reduce for simple accumulation
const total = items.reduce((sum, item) => sum + item.price, 0);

// ❌ INCORRECT - Let with mutation at top level
let total = 0;
for (const item of items) {
  total += item.price;
}

// ✅ CORRECT - Const with functional composition
const result = (() => {
  const filtered = items.filter(item => item.active);
  const mapped = filtered.map(item => item.value);
  return mapped.reduce((sum, val) => sum + val, 0);
})();

// ❌ INCORRECT - Multiple let mutations
let filtered: Item[] = [];
for (const item of items) {
  if (item.active) {
    filtered.push(item);
  }
}
let mapped: number[] = [];
for (const item of filtered) {
  mapped.push(item.value);
}

Type Annotations

Explicitly Annotate Types Where Practicable

typescript
// ✅ CORRECT - Explicit return type and parameter types
function calculateTotal(items: CartItem[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// ✅ CORRECT - Use library types
import type { Request, Response } from 'express';

async function handler(req: Request, res: Response): Promise<void> {
  res.json({ success: true });
}

// ✗ AVOID - Implicit types in complex functions
function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// ✅ CORRECT - Type interfaces for data structures
interface User {
  id: string;
  name: string;
  email: string;
  age: number;
}

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

Use Library Types, Don't Make Up Types

typescript
// ✅ CORRECT - Import and use library types
import type { Server } from 'http';
import type { Socket } from 'net';

function startServer(server: Server): void {
  // ...
}

// ❌ INCORRECT - Creating your own types for well-known libraries
interface MyServer {
  on: (event: string, callback: Function) => void;
}

function startServer(server: MyServer): void {
  // ...
}

Avoid Type Casting

typescript
// ❌ INCORRECT - Never use 'as any'
const user = data as any;

// ❌ INCORRECT - Avoid 'as unknown' if a known type exists
const user = data as unknown as User;

// ✅ CORRECT - Proper type annotations upstream
interface ApiResponse<T> {
  data: T;
  error?: string;
}

async function fetchUser(id: string): Promise<ApiResponse<User>> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// ✅ CORRECT - Type guards for runtime checks
function isUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    'name' in data
  );
}

if (isUser(data)) {
  // TypeScript knows data is User here
  console.log(data.name);
}

// ✅ CORRECT - Generic functions for flexible typing
function processResponse<T>(response: ApiResponse<T>): T | null {
  return response.error ? null : response.data;
}

Use Zod for Validation

typescript
// ❌ INCORRECT - Manual JSON validation
function validateUser(data: unknown): User | null {
  if (typeof data !== 'object' || data === null) return null;
  const user = data as Record<string, unknown>;
  if (typeof user.id !== 'string') return null;
  if (typeof user.name !== 'string') return null;
  return { id: user.id, name: user.name };
}

// ✅ CORRECT - Use Zod for validation
import { z } from 'zod';

const ZUser = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(0),
});

type User = z.infer<typeof ZUser>;

function validateUser(data: unknown): User | null {
  const result = ZUser.safeParse(data);
  return result.success ? result.data : null;
}

Docstrings

typescript
// ✅ CORRECT - Always add docstring to every top-level class
/**
 * Manages user authentication and session handling.
 * Provides methods for login, logout, and token refresh.
 */
class AuthManager {
  constructor(private readonly config: AuthConfig) {}
}

// ✅ CORRECT - Always add docstring to every top-level function
/**
 * Calculates the total price of items in a cart.
 * @param items - Array of cart items to calculate
 * @returns The total price as a number
 */
function calculateTotal(items: CartItem[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// ✅ CORRECT - Docstring with complex parameter description
/**
 * Processes a batch of user records with configurable options.
 * @param params - Processing configuration
 * @param params.users - Array of users to process
 * @param params.batchSize - Number of users to process at once (default: 100)
 * @param params.retryFailed - Whether to retry failed operations (default: false)
 * @returns Object with success and failure counts
 */
async function processUsers(params: ProcessUsersParams): Promise<ProcessUsersResult> {
  // ...
}

// ✅ OK - No docstring needed for subfunctions defined inside a function
function processData(data: unknown): ProcessedData {
  // Subfunction validation - no docstring needed
  const validate = (input: unknown) =>
    input !== null && typeof input === 'object';

  // Subfunction transform - no docstring needed
  const transform = (raw: unknown): ProcessedData => ({
    id: '123',
    value: 0,
  });

  if (validate(data)) {
    return transform(data);
  }
  return { id: 'error', value: 0 };
}

Named Parameters

typescript
// ✅ CORRECT - Single function argument with named parameters for 2+ args
interface SendEmailParams {
  to: string;
  subject: string;
  body: string;
  attachments?: string[];
  priority?: 'low' | 'normal' | 'high';
}

/**
 * Sends an email with the given parameters.
 */
function sendEmail(params: SendEmailParams): Promise<void> {
  // ...
}

// Usage
await sendEmail({
  to: 'user@example.com',
  subject: 'Welcome',
  body: 'Hello!',
  priority: 'high'
});

// ✅ CORRECT - Single field functions can use direct parameters
interface EmailParam {
  email: string;
}

/**
 * Validates an email address format.
 */
function validateEmail(email: string): boolean {
  // No object parameter for single field
}

// ❌ INCORRECT - Multiple parameters without object wrapper
function sendEmail(
  to: string,
  subject: string,
  body: string,
  attachments?: string[],
  priority?: 'low' | 'normal' | 'high'
): Promise<void> {
  // ...
}

// ✅ CORRECT - Define param interface immediately before function
interface FetchUserParams {
  id: string;
  includeProfile?: boolean;
}

async function fetchUser(params: FetchUserParams): Promise<User> {
  // ...
}

Import Style

typescript
// ✅ CORRECT - Sibling absolute import path
import { utils } from './lib/utils';
import { Button } from './components/Button';
import { API } from '../shared/api';

// ✅ CORRECT - Absolute import path with @/ alias
import { API } from '@/shared/api';
import { formatCurrency } from '@/utils/currency';

// ❌ INCORRECT - Relative parent import path
import { API } from '../../shared/api';
import { formatCurrency } from '../../../utils/currency';

// ✅ CORRECT - Group and order imports logically
import React, { useState } from 'react';
import type { NextPage } from 'next';

import { Button } from '@/components/Button';
import { Input } from '@/components/Input';

import { formatCurrency } from '@/utils/currency';
import { formatDate } from '@/utils/date';

import type { User } from '@/types';
import type { Product } from '@/types';

import styles from './ProductList.module.css';

Error Handling

typescript
// ✅ CORRECT - Try/catch only at root calling function
/**
 * Processes a user request from start to finish.
 * All intermediate errors are propagated and handled here.
 */
async function handleUserRequest(requestId: string): Promise<void> {
  try {
    const data = await fetchData(requestId);
    const processed = await processData(data);
    await saveResult(processed);
  } catch (error) {
    console.error('Failed to handle user request:', error);
    throw error;
  }
}

// ❌ INCORRECT - Try/catch in intermediate functions
async function fetchData(requestId: string): Promise<Data> {
  try {
    const response = await fetch(`/api/data/${requestId}`);
    return response.json();
  } catch (error) {
    console.error('Failed to fetch data:', error);
    throw error;
  }
}

// ❌ INCORRECT - Try/catch in leaf functions
async function saveResult(result: ProcessedData): Promise<void> {
  try {
    await db.save(result);
  } catch (error) {
    console.error('Failed to save result:', error);
    throw error;
  }
}

Multiline Strings with dedent

typescript
// ✅ CORRECT - Use dedent for nicely formatted multiline strings
import dedent from 'dedent';

const message = dedent`
  Welcome to our service!

  Here are your options:
  - Option 1: Do something
  - Option 2: Do something else

  Thanks for choosing us!
`;

const sql = dedent`
  SELECT
    users.id,
    users.name,
    users.email
  FROM users
  WHERE users.active = true
    AND users.created_at > NOW() - INTERVAL '30 days'
  ORDER BY users.name
  LIMIT 100
`;

// ❌ INCORRECT - Manual indentation or template literals
const message = `
  Welcome to our service!

  Here are your options:
  - Option 1: Do something
  - Option 2: Do something else

  Thanks for choosing us!
`;

const sql = `SELECT
  users.id,
  users.name,
  users.email
FROM users
WHERE users.active = true`;

Example: Complete File Following All Rules

typescript
import dedent from 'dedent';
import type { Request, Response } from 'express';
import { db } from '@/lib/db';
import { logger } from '@/lib/logger';

/**
 * Parameters for processing a bulk user export.
 */
interface ProcessBulkExportParams {
  userIds: string[];
  format: 'csv' | 'json';
  includeProfile?: boolean;
}

/**
 * Result of a bulk export operation.
 */
interface ProcessBulkExportResult {
  totalUsers: number;
  successfulExports: number;
  failedExports: number;
  outputFile?: string;
}

/**
 * Processes a bulk user export request.
 * Validates inputs, fetches user data, and generates export file.
 */
async function handleBulkExport(
  req: Request,
  res: Response
): Promise<void> {
  try {
    const params: ProcessBulkExportParams = req.body;
    const result = await processBulkExport(params);

    res.json({
      success: true,
      data: result,
    });
  } catch (error) {
    logger.error('Bulk export failed:', error);
    res.status(500).json({
      success: false,
      error: 'Bulk export failed',
    });
  }
}

/**
 * Processes a bulk export of user data.
 * @param params - Export configuration parameters
 * @returns Export operation results
 */
async function processBulkExport(
  params: ProcessBulkExportParams
): Promise<ProcessBulkExportResult> {
  const users = await fetchUsersByIds(params.userIds);

  if (params.includeProfile) {
    const enrichedUsers = await enrichUsersWithProfiles(users);
    return generateExport(enrichedUsers, params.format);
  }

  return generateExport(users, params.format);
}

/**
 * Fetches users by their IDs from the database.
 */
async function fetchUsersByIds(userIds: string[]): Promise<User[]> {
  const records = await db.users.where('id').in(userIds).toArray();
  return records.map(record => mapDbRecordToUser(record));
}

/**
 * Enriches user records with their profile data.
 */
async function enrichUsersWithProfiles(users: User[]): Promise<EnrichedUser[]> {
  const profileIds = users.map(user => user.profileId);
  const profiles = await fetchProfilesByIds(profileIds);
  const profileMap = Object.fromEntries(
    profiles.map(profile => [profile.id, profile])
  );

  return users.map(user => ({
    ...user,
    profile: profileMap[user.profileId],
  }));
}

/**
 * Generates an export file in the specified format.
 */
async function generateExport(
  users: User[],
  format: 'csv' | 'json'
): Promise<ProcessBulkExportResult> {
  const successfulUsers = users.filter(user => user.active);
  const failedUsers = users.filter(user => !user.active);

  if (format === 'csv') {
    const csvContent = generateCsv(successfulUsers);
    const outputFile = await writeExportFile(csvContent, 'users.csv');

    return {
      totalUsers: users.length,
      successfulExports: successfulUsers.length,
      failedExports: failedUsers.length,
      outputFile,
    };
  }

  const jsonContent = JSON.stringify(successfulUsers, null, 2);
  const outputFile = await writeExportFile(jsonContent, 'users.json');

  return {
    totalUsers: users.length,
    successfulExports: successfulUsers.length,
    failedExports: failedUsers.length,
    outputFile,
  };
}

/**
 * Generates CSV content from user records.
 */
function generateCsv(users: User[]): string {
  const headers = ['id', 'name', 'email', 'active'];
  const rows = users.map(user =>
    [user.id, user.name, user.email, user.active.toString()].join(',')
  );

  return dedent`
    ${headers.join(',')}
    ${rows.join('\n')}
  `;
}

/**
 * Writes export content to a file.
 */
async function writeExportFile(
  content: string,
  filename: string
): Promise<string> {
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  const outputFile = `/exports/${timestamp}-${filename}`;

  await Bun.write(outputFile, content);
  return outputFile;
}

export { handleBulkExport, processBulkExport, generateExport };