ban-type-assertions について
TypeScriptの型アサーションを禁止するための開発スキルです。ESLintルールを有効にし、`as`キャストをコンパイラーが検証できる方法に置き換えます。データ境界でのスキーマ検証や、関連する型、テスト、パッケージの確認も案内します。
主な機能
- 01型アサーション禁止のESLintルール設定
- 02Zodによる境界データの解析
- 03制御フローによるユニオン型の絞り込み
- 04共有スキーマの確認と統合
- 05lint、型チェック、テストの実行
活用シーン
- 01TypeScriptプロジェクトから`as`キャストを除去
- 02JSON、ディスク、ネットワーク由来のデータ検証
- 03アサーションルール導入後のテスト修正
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ factory-ai/ factory-plugins --skill ban-type-assertionsインストール後、Claude Code で/ban-type-assertionsを入力して読み込まれたか確認します。
詳細
型アサーションを禁止する
パッケージで assertionStyle: 'never' を設定して @typescript-eslint/consistent-type-assertions を有効にし、すべての as X キャストをコンパイラーが検証できるパターンに置き換えます。
基本方針
より簡単な方法ではなく、厳密に正しい方法を選びます。
すべての as アサーションは、開発者がコンパイラーに「信じてください」と伝える箇所です。目指すのは、代わりにコンパイラーに検証させることです。as Foo を同じく未検証の型ガードに置き換えても、何も改善していません。アサーションの場所を移しただけです。
クイックリファレンス
- ルール:
@typescript-eslint/consistent-type-assertions - 設定:
{ assertionStyle: 'never' } - 場所:
packages/<name>/.eslintrc.js
ワークフロー
1. ルールを有効にする
パッケージの .eslintrc.js に追加します。
rules: {
'@typescript-eslint/consistent-type-assertions': ['error', { assertionStyle: 'never' }],
}
2. 違反箇所を列挙する
cd packages/<name> && npm run lint 2>&1 | grep "consistent-type-assertions"
修正する前に、ファイルとパターンごとに違反箇所を分類します。
3. 修正前に調査する
置き換えコードを書く前に、以下を確認します。
- 既存の zod スキーマを確認する —
@factory/commonとリポジトリ全体で、型名と併せてSchemaを検索します。 - スキーマが存在してもエクスポートされていない場合は確認する — その場合は、新しく作成するのではなくエクスポートします。
- パッケージ間で重複した型/インターフェースを確認する — 見つかった場合は、
@factory/commonに統合します。 - データフローを把握する — これはパース境界(外部データ)、型の絞り込み箇所(ユニオン型)、それともライブラリの型の不足でしょうか。
4. パターンの優先順位に従って違反を修正する
優先度 1:Zod によるパース(外部データとの境界向け)
JSON、ディスク、ネットワーク、IPC などからシステムに入るデータには、すべてこれを使用します。型注釈だけでなく、実行時検証を行えます。
// BAD
const data = JSON.parse(raw) as MyType;
// GOOD
const data = MySchema.parse(JSON.parse(raw));
エラーを適切に処理する必要がある場合(リクエスト ID などのコンテキストを含むエラーレスポンスを返す場合など)は、safeParse を使用します。
// BAD: throws before you can extract the request id
const request = RequestSchema.parse(JSON.parse(raw));
// GOOD: safeParse lets you return a proper error
const parsed = RequestSchema.safeParse(JSON.parse(raw));
if (!parsed.success) {
return errorResponse(rawObj?.id ?? null, INVALID_PARAMS, parsed.error.message);
}
const request = parsed.data;
優先度 2:制御フローによる型の絞り込み(ユニオン型向け)
switch、in、instanceof、または判別可能なユニオンを使用します。
// BAD
(error as NodeJS.ErrnoException).code
// GOOD
if (error instanceof Error && 'code' in error) {
const code = error.code;
}
// BAD
if (METHODS.has(method as Method)) { ... }
// GOOD: switch narrows exhaustively
switch (method) {
case 'foo':
case 'bar':
return handle(method); // narrowed
}
優先度 3:根拠を示した eslint-disable(最後の手段)
本当に避けられないケース(ライブラリの型の不足、推論できないジェネリック型パラメーターなど)に限ります。必ず理由を説明します。
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- ws library types require generic parameter
ws.on('message', handler);
アンチパターン:アサーションを偽装した型ガード
// NOT an improvement -- checks shape but not content
function isDaemonRequest(x: unknown): x is DaemonRequest {
return typeof x === 'object' && x !== null && 'method' in x;
}
Zod スキーマは値を検証します。このような型ガードは、手順を増やしただけの未検証アサーションです。絞り込みのロジックが本当に十分な場合に限り、型ガードを使用します。
5. 寛容なスキーマではなく、厳密なスキーマを使用する
スキーマ(例:SessionSettingsSchema)が存在する場合は、z.record(z.unknown()) ではなく、そのスキーマを厳密に使用します。これにより、前方互換性が保たれます。マイグレーションでフィールドが削除されていれば、読み込み時に古いデータがクリーンアップされます。
// BAD: accepts anything
const settings = z.record(z.unknown()).parse(raw);
// GOOD: validates against the real shape
const settings = SessionSettingsSchema.parse(raw);
6. 共有スキーマを @factory/common に昇格する
パッケージ間で重複したインターフェース、型、スキーマを見つけた場合は、統合します。
@factory/common/<domain>/<subdomain>/schema.tsにスキーマを作成します- 列挙型は兄弟ファイルの
enums.tsに配置します(factory/enum-file-organizationにより必須) index.tsのバレルではなく、サブパス(例:@factory/common/session/summary)経由でエクスポートします- ローカルにある重複定義をすべて削除します
- すべての利用側を更新し、共通のサブパスからインポートするようにします
- リポジトリのルートで
npm run knipを実行し、未使用のバレル再エクスポートを検出します
7. テスト用モックをスキーマに合わせて修正する
as X を .parse() に置き換えると、アサーションに依存していたテスト用モックは検証に失敗します。モックを修正してください。テストではルールを無効にしないでください。
有効なテスト用フィクスチャを一元管理するヘルパー関数を作成します。
function mockSessionSummary(
overrides?: Partial<SessionSummaryEvent>,
): SessionSummaryEvent {
return {
type: 'session_start',
id: 'test-id',
title: 'Test Session',
owner: 'test-owner',
...overrides,
};
}
8. エラー処理の中で、境界部分でパースする
パースの失敗が未処理の例外ではなく、適切なエラーレスポンスにつながる場所でパースが行われるようにします。
// BAD: parse outside try/catch -- if it throws, you lose context
const request = RequestSchema.parse(data);
try { handle(request); } catch { ... }
// GOOD: safeParse before try, handle error with context
const parsed = RequestSchema.safeParse(data);
if (!parsed.success) {
return errorResponse(rawData?.id ?? null, INVALID_PARAMS, parsed.error.message);
}
try { handle(parsed.data); } catch { ... }
検証
影響を受けるすべてのパッケージを対象に実行します(@factory/common の変更が、下流のパッケージの lint を失敗させる可能性があります)。
# Lint (all affected packages)
cd packages/<name> && npm run lint
# Typecheck
npm run typecheck
# Tests
npm run test
# Unused exports (repo root)
npm run knip
注意事項
factory/enum-file-organizationでは、TypeScript の列挙型をenums.tsという名前のファイルに配置する必要がありますno-barrel-filesは、バレルファイルから型を再エクスポートすることを防ぎます。利用側はサブパスから直接インポートする必要があります- 型を common に移す際、新しいサブパスの
package.jsonエクスポートエントリーが存在しない場合は追加してください - テストファイルでモックのセットアップにアサーション構文を使用している場合、
.eslintrc.jsでこのルールのテスト用例外設定が必要になることがあります。ただし、ルールを無効にするより、モックを修正することを優先してください