mcpField
T
typescript-project

typescript-project

github.com/majiayu000開発28885 回インストール今日更新
リポジトリ

typescript-project について

厳格な型付け、ESM、レイヤー構成、バリデーション、テストを軸に TypeScript プロジェクトを設計するスキルです。新規構築や保守を行う開発者向けに、互換レイヤーを避け、実際の動作を検証する方針も示します。

主な機能

  1. 01Zod による厳格な型と検証
  2. 02Node 22+ または Bun の ESM
  3. 03lib・services・adapters のレイヤー構成
  4. 04Vitest または Bun test の指針
  5. 05後方互換性を設けない方針
  6. 06LLM 向け LiteLLM プロキシの指針

活用シーン

  1. 01TypeScript プロジェクト構成の開始
  2. 02ビジネスロジックと外部連携の分離
  3. 03入力とエラー動作の検証
  4. 04サービスコードのテスト選定

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/majiayu000/spellbook --skill typescript-project

インストール後、Claude Code で/typescript-projectを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

reference/
architecture.md
patterns.md
tech-stack.md
templates/project/
biome.json
package.json
tsconfig.json
templates/project/src/
index.ts
templates/project/src/adapters/
in-memory.repository.ts
llm.adapter.ts
templates/project/src/lib/
config.ts
errors.ts
index.ts
logger.ts
types.ts
templates/project/src/services/
example.service.ts
templates/project/tests/
example.service.test.ts

詳細

TypeScript プロジェクトのアーキテクチャ

基本原則

  • 型安全性を最優先 — Strict モードを有効にし、any を使わず、実行時検証には Zod を使用します
  • ネイティブ ESM — デフォルトで ES Modules を使用します。Node 22+ / Bun に対応します
  • レイヤードアーキテクチャ — lib/services/adapters を分離します
  • 200 行の上限 — 200 行を超えるファイルは作りません(elegant-architecture スキルを参照)
  • 現実に即したテスト — Vitest/Bun test を使用し、モックは最小限にします
  • 後方互換性は維持しない — 非推奨にするのではなく削除します。シムを設けず、直接変更します
  • LLM API には LiteLLM を使用 — 特定の SDK が必要な場合を除き、すべての LLM 統合に LiteLLM プロキシを使用します

注意点

  • Bun、Zod、ディレクトリ構成、LiteLLM の選択は、作業開始時の観察事項であり、タスクの成功を証明するものではありません。有効な入力、無効な入力、状態の変化、実際のエラー仕様を確認してください。
  • テンプレートはサービスのテストに合格しても、typecheck に失敗することがあります。2026 年 10 月の事例では、メンテナンスされているテンプレートの noUnusedLocals により、未使用の設定用ヘルパーが検出されました。両方を実行してください。
  • latest の依存関係宣言では、再現した実行のバージョンは固定されません。生成されたロックファイルをその証跡とともに保管し、実際にインストールされたバージョンを記録してください。別のランタイムでテストに合格したと主張しないでください。
  • プロキシ URL を設定しても、モデルの実行結果が得られたことにはなりません。プロバイダーがない場合は、アプリケーションの独立した動作を検証し、LLM の確認は未完了であることを明記してください。

後方互換性は維持しない

未使用のコードを削除します。直接変更します。互換性レイヤーは設けません。

理由
  • デッドコードは技術的負債になります
  • 互換性シムは複雑さを増します
  • 古いパターンはコピー&ペーストで広がります
  • 「一時的な」回避策が恒久化します
避けるべきアンチパターン
// ❌ BAD: Renaming but keeping old export
export { newName };
export { newName as oldName }; // "for backwards compatibility"

// ❌ BAD: Unused parameter with underscore
function process(_legacyParam: string, data: Data) { ... }

// ❌ BAD: Deprecated comments instead of deletion
/** @deprecated Use newMethod instead */
export function oldMethod() { ... }

// ❌ BAD: Re-exporting removed functionality
export { removed } from './legacy'; // Keep for existing consumers

// ❌ BAD: Feature flags for old behavior
if (config.useLegacyMode) { ... }
正しいアプローチ
// ✅ GOOD: Just delete and update all usages
// Old: export { fetchData as getData }
// New: export { fetchData }
// Then: Find & replace all getData → fetchData

// ✅ GOOD: Remove unused parameters entirely
function process(data: Data) { ... }

// ✅ GOOD: Delete deprecated code, update callers
// Don't mark as deprecated, just remove it

// ✅ GOOD: Breaking changes are fine in active development
// Semantic versioning handles this for libraries
インターフェースを変更する場合
// ❌ BAD: Adding optional fields "for compatibility"
interface User {
  id: string;
  name: string;
  firstName?: string; // New field, name kept for compatibility
  lastName?: string;
}

// ✅ GOOD: Clean break, update all usages
interface User {
  id: string;
  firstName: string;
  lastName: string;
}
// Then update ALL code that uses User.name
移行戦略
  1. すべての使用箇所を探す — grep -r "oldName" src/
  2. 一度にすべて更新する — 単一のコミットで行い、移行期間は設けません
  3. 古いコードを削除する — 非推奨の警告は追加せず、そのまま削除します
  4. テストを実行する — 何も壊れていないことを確認します

LLM API には LiteLLM を使用

すべての LLM 統合に LiteLLM プロキシを使用します。プロバイダーの API を直接呼び出さないでください。

LiteLLM を使う理由
  • 統一されたインターフェース — 100 以上の LLM プロバイダー(OpenAI、Anthropic、Azure、Bedrock など)に対応する単一の API
  • プロバイダーに依存しない — コードを変更せずにモデルを切り替えられます
  • コスト追跡 — 使用量とコストを組み込みで監視できます
  • ロードバランシング — プロバイダー間のフェイルオーバーを自動化します
  • レート制限 — クォータの枯渇を防ぎます
セットアップ
# Run LiteLLM proxy (Docker)
docker run -p 4000:4000 ghcr.io/berriai/litellm:main-stable

# Or install locally
pip install litellm[proxy]
litellm --model gpt-4o
TypeScript での使用方法
// adapters/llm.adapter.ts
import { OpenAI } from 'openai';

// Connect to LiteLLM proxy using OpenAI SDK
const llm = new OpenAI({
  baseURL: process.env.LITELLM_URL || 'http://localhost:4000',
  apiKey: process.env.LITELLM_API_KEY || 'sk-1234', // Proxy API key
});

export async function complete(prompt: string, model = 'gpt-4o'): Promise<string> {
  const response = await llm.chat.completions.create({
    model, // Can be any model: gpt-4o, claude-3-opus, gemini-pro, etc.
    messages: [{ role: 'user', content: prompt }],
  });
  return response.choices[0]?.message?.content ?? '';
}
LiteLLM を使わない場合
  • プロバイダー固有の機能を使ったストリーミング(例:Anthropic のツール使用時のストリーミング)
  • OpenAI 形式ではないプロバイダー固有の API(メタデータ付きの埋め込みなど)
  • コンプライアンスやセキュリティ上の理由で直接 SDK が必要な場合
アンチパターン
// ❌ BAD: Direct provider SDKs everywhere
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { GoogleGenerativeAI } from '@google/generative-ai';

// ❌ BAD: Provider-specific code scattered across codebase
if (provider === 'anthropic') { ... }
else if (provider === 'openai') { ... }

// ✅ GOOD: Single LiteLLM adapter, switch models via config
const response = await llm.chat.completions.create({
  model: config.llmModel, // "gpt-4o" or "claude-3-opus" or "gemini-pro"
  messages,
});

クイックスタート

1. プロジェクトを初期化する
# Using Bun (recommended)
bun init
bun add zod
bun add -d typescript @types/bun @biomejs/biome

# Using Node.js
npm init -y
npm i zod
npm i -D typescript @types/node tsx @biomejs/biome
2. 技術スタックを適用する
レイヤー推奨
ランタイムBun / Node 22+
言語TypeScript(最新)
バリデーションZod(最新)
テストBun test / Vitest
ビルドbun build / tsup
リンティングBiome(最新)
バージョン戦略

常に最新バージョンを使用します。テンプレートではバージョンを固定しないでください。

{
  "dependencies": {
    "zod": "latest"
  },
  "devDependencies": {
    "@biomejs/biome": "latest",
    "typescript": "latest"
  }
}
  • bun add / npm i は最新バージョンを自動的に取得します
  • bun update --latest を使ってすべての依存関係をアップグレードします
  • ロックファイル(bun.lockb、package-lock.json)によって、再現可能なビルドを実現します
  • 破壊的変更は、更新を避けるのではなく、変更履歴を読んで対処します
3. 標準構成を使用する
project/
├── src/
│   ├── index.ts           # Entry point
│   ├── lib/               # Core utilities
│   │   ├── config.ts      # Configuration management
│   │   ├── errors.ts      # Custom error classes
│   │   ├── logger.ts      # Logging infrastructure
│   │   └── types.ts       # Shared type definitions
│   ├── services/          # Business logic
│   │   └── *.service.ts
│   └── adapters/          # External integrations
│       └── *.adapter.ts
├── tests/                 # Test files
│   └── *.test.ts
├── tsconfig.json
├── package.json
└── biome.json             # or eslint.config.js

アーキテクチャのレイヤー

lib/ — コアインフラストラクチャ

アプリケーション全体で使用する基盤コードです。

// lib/types.ts — Shared type definitions
export interface Result<T, E = Error> {
  ok: boolean;
  data?: T;
  error?: E;
}

// lib/errors.ts — Custom errors
export class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500
  ) {
    super(message);
    this.name = 'AppError';
  }
}

// lib/config.ts — Configuration
export const config = {
  env: process.env.NODE_ENV || 'development',
  port: Number(process.env.PORT) || 3000,
  db: {
    url: process.env.DATABASE_URL!,
  },
} as const;

// lib/logger.ts — Logging (see structured-logging-lite skill)
services/ — ビジネスロジック

依存関係を注入する、純粋なビジネスロジックです。

// services/user.service.ts
export class UserService {
  constructor(private readonly userRepo: UserRepository) {}

  async create(input: CreateUserInput): Promise<User> {
    const existing = await this.userRepo.findByEmail(input.email);
    if (existing) throw new AppError('Email exists', 'USER_EXISTS', 409);
    return this.userRepo.save(User.create(input));
  }
}
adapters/ — 外部との統合

外部システム(DB、API、ファイルシステム)とのインターフェースを担います。

// adapters/postgres.adapter.ts
export class PostgresUserRepository implements UserRepository {
  constructor(private readonly db: Database) {}

  async findByEmail(email: string): Promise<User | null> {
    const row = await this.db.query('SELECT * FROM users WHERE email = $1', [email]);
    return row ? User.fromRow(row) : null;
  }
}

設定ファイル

tsconfig.json(2025)
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "dev": "bun run --watch src/index.ts",
    "build": "bun build src/index.ts --outdir dist --target bun",
    "start": "bun dist/index.js",
    "test": "bun test",
    "typecheck": "tsc --noEmit"
  }
}

Zod によるバリデーション

import { z } from 'zod';

// Define schemas
export const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(100),
  age: z.number().int().positive().optional(),
});

// Infer types from schemas
export type CreateUserInput = z.infer<typeof CreateUserSchema>;

// Validate at boundaries
export function validateInput<T>(schema: z.ZodType<T>, data: unknown): T {
  return schema.parse(data);
}

エラーハンドリングのパターン

// lib/errors.ts
export class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500,
    public readonly context?: Record<string, unknown>
  ) {
    super(message);
    this.name = 'AppError';
    Error.captureStackTrace(this, this.constructor);
  }

  static notFound(resource: string, id: string) {
    return new AppError(`${resource} not found: ${id}`, 'NOT_FOUND', 404);
  }

  static validation(message: string, context?: Record<string, unknown>) {
    return new AppError(message, 'VALIDATION_ERROR', 400, context);
  }
}

// Usage
throw AppError.notFound('User', userId);

テスト戦略

// tests/user.service.test.ts
import { describe, it, expect, beforeEach } from 'bun:test';
import { UserService } from '../src/services/user.service';
import { InMemoryUserRepository } from './helpers/in-memory-repo';

describe('UserService', () => {
  let service: UserService;
  let repo: InMemoryUserRepository;

  beforeEach(() => {
    repo = new InMemoryUserRepository();
    service = new UserService(repo);
  });

  it('creates user with valid input', async () => {
    const user = await service.create({
      email: '[email protected]',
      name: 'Test User',
    });

    expect(user.email).toBe('[email protected]');
    expect(await repo.findByEmail('[email protected]')).toEqual(user);
  });

  it('rejects duplicate email', async () => {
    await service.create({ email: '[email protected]', name: 'User 1' });

    expect(
      service.create({ email: '[email protected]', name: 'User 2' })
    ).rejects.toThrow('Email exists');
  });
});

チェックリスト

## Project Setup
- [ ] TypeScript strict mode enabled
- [ ] ESM modules configured
- [ ] Biome/ESLint configured
- [ ] Testing framework ready

## Architecture
- [ ] lib/ for core utilities
- [ ] services/ for business logic
- [ ] adapters/ for external integrations
- [ ] Clear module boundaries

## Quality
- [ ] Zod schemas for validation
- [ ] Custom error classes
- [ ] Structured logging
- [ ] Tests for critical paths

## Build
- [ ] Build script configured
- [ ] Type checking in CI
- [ ] Tests in CI

See Also

コメント