コンテンツにスキップ

検索はプロダクションビルドでのみ利用可能です。 ローカルでテストするには、サイトをビルドしてプレビューしてください。

コーディングルール

このドキュメントは Speakeasy プロジェクトにおける厳守必須のコーディングルールです。すべての開発者(人間・AI 含む)がこのルールに従う必要があります。


// OK
import { createDrizzleClient } from '@speakeasy/cellar';
// NG — require は禁止
const cellar = require('@speakeasy/cellar');

パッケージの公開 API からのみインポートします。内部モジュールへの直接アクセスは禁止です。

// OK — 公開 API からインポート
import { contactSchema } from '@speakeasy/jigger/schemas';
// NG — 内部パスへの直接アクセス
import { contactSchema } from '@speakeasy/jigger/src/schemas/contact';

すべてのディレクトリに index.ts を配置し、公開メンバーのみをエクスポートします。

// src/utils/index.ts — エクスポートのみ、ロジック禁止
export { formatDate } from './format-date';
export { generateId } from './generate-id';
export type { DateFormat } from './types';
// OK
export const createClient = () => {
/* ... */
};
// NG — export default は禁止
export default function createClient() {
/* ... */
}

唯一の例外は Cloudflare Workers の WorkerEntrypoint と Durable Objects です。

// OK — 関数と型で構成
const createContactService = (params: { readonly db: DrizzleClient }) => {
// ...
};
// NG — クラスは禁止
class ContactService {
constructor(private db: DrizzleClient) {}
}
// OK — as const オブジェクト
const MESSAGE_TYPE = {
NORMAL: 'normal',
BROADCAST: 'broadcast',
SYSTEM: 'system'
} as const;
type MessageType = (typeof MESSAGE_TYPE)[keyof typeof MESSAGE_TYPE];
// OK — Union 型
type UserRole = 'admin' | 'user';
// NG — enum は禁止
enum MessageType {
NORMAL = 'normal',
BROADCAST = 'broadcast'
}
// OK — const のみ使用
const count = items.length;
const result = items.map((item) => item.name);
// NG — let/var は禁止
let count = 0;
var result = [];
// OK — readonly プロパティ
type Contact = {
readonly id: string;
readonly name: string;
readonly tags: readonly string[];
};
// NG — mutable プロパティ
type Contact = {
id: string;
name: string;
tags: string[];
};

2つ以上の引数がある関数は、単一のオブジェクト引数を使用します。

// OK — オブジェクト引数
const sendMessage = (params: {
readonly roomId: string;
readonly content: string;
readonly attachments?: readonly string[];
}) => {
/* ... */
};
// NG — 個別引数
const sendMessage = (roomId: string, content: string) => {
/* ... */
};
// OK — 定数定義
const HEARTBEAT_INTERVAL_MS = 30_000;
const MAX_RECONNECT_ATTEMPTS = 10;
// NG — マジックナンバー
setInterval(heartbeat, 30000);

// OK — ResultAsync(neverthrow)
import { ResultAsync, errAsync, okAsync } from 'neverthrow';
const findContact = (params: { readonly id: string }): ResultAsync<Contact, AppError> => {
// ...
};
// OK — 判別共用体
type FindResult =
| { readonly ok: true; readonly data: Contact }
| { readonly ok: false; readonly error: AppError };
// NG — throw は禁止
const findContact = (id: string): Contact => {
throw new Error('Not found');
};

例外: アンチコラプションレイヤー(外部ライブラリとの境界)でのみ許可されます。

// OK — エッジのアンチコラプションレイヤーでのみ許可
const parseExternalResponse = (raw: unknown): Result<Data, ParseError> => {
try {
// 外部ライブラリの呼び出し
const parsed = externalLib.parse(raw);
return ok(parsed);
} catch (e) {
return err({ type: 'parse_error', message: String(e) });
}
};
// OK
const result = contactSchema.safeParse(input);
if (!result.success) {
return err(result.error);
}
// NG — parse は禁止(例外を throw するため)
const data = contactSchema.parse(input);
// OK — 型ガード
const isContact = (value: unknown): value is Contact => {
return typeof value === 'object' && value !== null && 'id' in value;
};
// NG — as Type は禁止
const contact = data as Contact;
// NG — ! は禁止
const name = contact.name!;

フラットコード(ネスト深度 1)

Section titled “フラットコード(ネスト深度 1)”
// OK — ガード節で早期リターン
const processContact = (contact: Contact | null) => {
if (!contact) return err({ type: 'not_found' });
if (!contact.isActive) return err({ type: 'inactive' });
return ok(contact);
};
// NG — ネストが深い
const processContact = (contact: Contact | null) => {
if (contact) {
if (contact.isActive) {
return ok(contact);
}
}
return err({ type: 'error' });
};

サーバーサイドのロジックは実際の DB を使った統合テストを行います。DB 操作のモックは避けてください。

// OK — 実 DB テスト
describe('連絡先作成', () => {
it('連絡先を作成し DB に保存される', async () => {
const result = await createContact({
db: testDb,
params: { name: '田中太郎', shopId: testShopId }
});
expect(result.isOk()).toBe(true);
const saved = await testDb.query.contact.findFirst({
where: eq(contact.id, result.value.id)
});
expect(saved).toBeDefined();
});
});
対象ツール実行
ユニットテストVitestbun test
Workers テスト@cloudflare/vitest-pool-workersbun test
ブラウザテストPlaywright + Vitest Browserbun test
Storybook テスト@storybook/addon-vitestbun test

対象言語
コードコメント日本語
TSDoc日本語
変数名・関数名英語
ドキュメント日本語
コミットメッセージ日本語

すべてのパブリック関数に TSDoc を記述します:

/**
* 連絡先を ID で検索する
*
* @param params - 検索パラメータ
* @param params.id - 連絡先 ID
* @param params.db - Drizzle クライアント
* @returns 連絡先データ、見つからない場合は NotFoundError
* @example
* ```typescript
* const result = await findContactById({ id: 'contact_123', db });
* if (result.isOk()) {
* console.log(result.value.name);
* }
* ```
*/
const findContactById = (params: {
readonly id: string;
readonly db: DrizzleClient;
}): ResultAsync<Contact, NotFoundError> => {
// ...
};

すべてのワークスペースパッケージは @speakeasy/ プレフィックスを使用:

import { createDrizzleClient } from '@speakeasy/cellar';
import { contactSchema } from '@speakeasy/jigger/schemas';
import { Button } from '@speakeasy/glass';
const items = ['a', 'b'];
// NG — 外部スコープの items をシャドウイング
items.map((items) => items.toUpperCase());
// OK
items.map((item) => item.toUpperCase());

ルール一覧(チェックリスト)

Section titled “ルール一覧(チェックリスト)”
#ルールカテゴリ
1ES Modules のみ(require 禁止)モジュール
2サブパスインポート禁止モジュール
3バレルエクスポート(index.tsモジュール
4index.ts はエクスポートのみモジュール
5export default 禁止モジュール
6クラス禁止関数型
7Enum 禁止関数型
8let/var 禁止関数型
9readonly 必須関数型
10オブジェクト引数パターン関数型
11変数シャドウイング禁止関数型
12マジックナンバー禁止関数型
13throw 禁止エラー
14try/catch 禁止(ACL 除く)エラー
15ResultAsync / 判別共用体エラー
16safeParse のみエラー
17型アサーション禁止エラー
18ネスト深度 1(ガード節)エラー
19実 DB 統合テストテスト
20TSDoc 必須(日本語)ドキュメント