typescript-write について
コードベースの開発者向けTypeScript・JavaScript開発スキルです。厳密な型付けと正確な型モデルを重視し、型チェックを必須としています。
主な機能
- 01編集コードで明示・暗黙の `any` を禁止
- 02型なし値の絞り込みを案内
- 03型モデルとAPI契約の指針
- 04`bun run type-check-pure` による検証を必須化
活用シーン
- 01`any` を持ち込まずにTypeScriptやTSXを編集
- 02APIデータやnullable状態を正確にモデル化
- 03型アサーションや関数シグネチャを確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ metabase/ metabase --skill typescript-writeインストール後、Claude Code で/typescript-writeを入力して読み込まれたか確認します。
詳細
TypeScript/JavaScript 開発スキル
テストのみの変更を含め、フロントエンドを編集する前に、frontend/AGENTS.md、docs/developers-guide/frontend.md、および以下の共通リファレンスを読んでください。新規または編集したテストについては、テストの作成に従ってください。
@./../_shared/development-workflow.md @./../_shared/typescript-commands.md @./../_shared/react-redux-patterns.md
any 禁止 — 厳守ルール
- 新規、移動、その他編集したコードに、明示的・暗黙的を問わず
anyを含めてはいけません。anyの型注釈、as any/as unknown as、anyと推論される型指定のないパラメーターや戻り値、暗黙的にanyとなる分割代入や配列/オブジェクトリテラルは禁止です。 - 型指定のない値は境界で食い止めてください。 標準の型、型付きラッパー、または検証や絞り込みを伴う
unknownを使用してください。ライブラリのデフォルト型や推論された型を確認してください。型チェックに合格しても、anyがコードに伝播していないとは限りません。たとえば、型指定のないJSON.parseやレスポンスの.json()の結果はunknownとして受け取り、型付きの呼び出し元に渡したりフィールドにアクセスしたりする前に、型を絞り込むか検証してください。 - 最終的な結果だけでなく、受け取った値にも型を指定してください。 戻り値の型注釈を付けても、パラメーターや分割代入したバインディングの推論型は変わりません。型指定のないデータは
unknownとして受け取り、分割代入やフィールドへのアクセスの前に、型を絞り込むか検証してください。 - 型の検証は必須です。 TS/TSX の変更を終える前に、
bun run type-check-pureを実行してください。TypeScript LSP ツールを利用できる場合は、ホバーと定義へ移動を使って変更したシンボルも確認してください。利用できない場合は、LSP の確認を省略してください。
型の厳密化
- 型キャストと緩い
unknownを避ける — 代わりにシグネチャを修正します。 - 関数が広いオブジェクトの1つのフィールドだけを必要とするなら、そのフィールドを受け取る — 広いオブジェクトではなく。シグネチャが正しければ、キャストは多くの場合不要になります。
- キャストに頼る前に
Partial<T>、Pick<T, K>、Record<K, V>、ジェネリクスを検討する。 - 辞書のキーをデータに一致させる。 キーが既知の場合は有限のキーユニオンを使用します。オープンな辞書にはインデックスシグネチャ、
Record<string, T>、またはMapを使用できます。インデックスシグネチャは値の型を制約しますが、その表記をRecord<string, T>に変えても、存在しないキーへのアクセスが安全になるわけではありません。 - 値がそのまま流れ、呼び出し側が型を把握している場合は、props/コンポーネントをジェネリック(
<T>)にすることを優先する。 - 緩い型付けよりも
unknownを優先する とともに、使用前に絞り込みます —unknownの値は使用時点でのガードを強制します。 - オブジェクトリテラルには
satisfies— 拡張せずに準拠させる必要がある場合(設定オブジェクト、ルックアップマップ、判別可能リテラル)に使用します。: T(拡張する)やas T(安全でない)より優れています。 - 非 null アサーション(
!)を避ける。ガード、早期リターン、または?.を優先します。!は、非 null であることが証明可能かつ限定的な場合にのみ、コメントを付けて使用します。 - 見つからない可能性のあるインデックス参照をガードする。 配列や辞書は、推論された型が省略していても
undefinedを返すことがあります。キーと値の関係を保持する反復形式を使用し、実行時のキーが宣言された型に属することが既知の場合にのみkeyofをアサートします。 - 冗長な実行時強制変換は不要 — すでに型付けされた値を
Number()/String()/Boolean()でラップしないでください。 - 型ガードは
frontend/src/metabase-types/guards/に置く。ローカルで再定義しないでください。 - 避けられないキャストには実際の正当化コメントが必要です。
metabase/no-unjustified-type-castsルールは、直前に置かれた任意のコメントを受け入れます — そのキャストが安全である実際の理由を記載してください。レガシーな// Unjustified type cast. FIXMEプレースホルダーを決して書かないでください。これはルール導入以前のキャストにのみ存在し、それをコピーすると正当化されていないキャストをリンターからすり抜けさせてしまいます。キャストが正しい理由を説明できないなら、そのキャストは間違っています — 型を修正してください。 - 避けられないアサーションはローカルに保つ。 繰り返しまたは複雑なアサーションは、不変条件を強制しやすくする場合にヘルパーの背後に分離します。それを正当化する非自明な実行時の前提をテストしてください。自明なアサーションが自動的に新しいヘルパーやテストを必要とするわけではありません。実装のエラーを黙らせるためだけにパブリックシグネチャを弱めないでください。
型のモデリング
- 既存の型を再利用し、再宣言しないでください。
metabase-types/apiの正規 ID とドメインエンティティ型(FieldId、TableId、ConcreteTableId、SchemaNameなど)を使用し、主要なデータ構造はそれらをキーにしてください(new Map<ConcreteTableId, …>())。生成された型や API の型を複製しないでください — 組み合わせるか、派生させてください(Pick、Omit、インデックスアクセスSomeType["field"]、ReturnType)。ローカルのドメイン形状を導入する前に、正規の宣言を検索してください。型が直接エクスポートされていない場合は、その形状を再作成するのではなく、所有する関数、エンドポイント、またはフックから派生させてください。 - ジェネリクスは、実装が守れる約束でなければなりません。 呼び出し側が選択する
get<T>(): Tは、外部データに関する未チェックのアサーションを偽装してはなりません。unknownを返して検証するか、Tを確立するバリデータを受け入れてください。function empty<T>(): T[] { return []; }のようなファクトリは有効です。シグネチャにTが何回現れるかではなく、実装を評価してください。 - 偽の精度よりも正直な型を優先してください。 ジェネリクスは、実際の関係を表現する場合に使用してください。複雑な型が動作を正確にモデル化できない場合は、サポートされていない保証をアサートするのではなく、より単純な型または絞り込みを伴う
unknownを選択してください。 - 実際のデータコントラクトをモデル化し、型を狭く保ってください。 存在しない可能性があるキーにはオプションの
field?: T、キーが常に存在するが値が undefined になる可能性がある場合にのみfield: T | undefined、明示的な API の null には| nullを使用してください。広範なstring/number/ 緩いRecordよりもドメインのユニオンを優先してください。 - 型を定義または改良する際は API 実装を参照し、実際のデータ構造と一致するようにしてください。型キャストを検討する場合は、まず実際のデータ構造に一致するように型を改良すべきかどうかを検討してください。
- オプショナリティは不在を反映しなければなりません。 必須フィールドは必須のまま、オプションフィールドはオプションのままにしてください。型エラーを取り除くためだけではなく、アプリケーションに意味のあるデフォルトがある場合に入力を正規化してください。API 型では実際のワイヤ形状を保持してください。
- 内部状態をモデル化する際は、関連する不在をまとめて表現してください。 複数のフィールドが同時に存在したり消えたりする場合は、null 許容の包含オブジェクトまたは判別可能なユニオンを検討してください。データが実際にその形状でない限り、生の API 宣言を再形成しないでください。
- フォーマットを設計する際は、明示的な特殊状態を優先してください。
-1のようなセンチネル値が意味を隠す場合は、null 許容または名前付きユニオンバリアントを使用してください。動作が意図的に変更されない限り、確立されたプロトコルのセンチネル値を保持するか、明示的な境界で正規化してください。 - バリアント状態には判別可能なユニオンを使用し、網羅的なチェックを行ってください。 「N 個の形状のうちの 1 つ」を、オプションフィールドの寄せ集めではなくリテラルの判別子を持つユニオンとしてモデル化し、ts-pattern の
.exhaustive()で網羅することで、バリアントの追加がコンパイルエラーになるようにしてください:import { match } from "ts-pattern"; const result = match(status) .with({ type: "loading" }, () => <Spinner />) .with({ type: "error", error: P.select() }, (error) => <Error message={error.message} />) .with({ type: "success", data: P.select() }, (data) => <Content data={data} />) .exhaustive(); // Compile-time guarantee all cases handled - 定数からユニオン型を派生させてください(
as const+typeof/keyof)。これにより、型と値がずれることがありません。 - 変更が意図されていない箇所では
readonly/ 不変性を使用してください — コンポーネントの props、共有定数、エクスポートされた設定、変更されないパラメータなどです。変更しない入力にはreadonly T[]/ReadonlyArray<T>を優先してください。呼び出し側が所有するデータを変更する関数は、その動作を明示的にする必要があります。 - 実用的な場合は、完全で適切に型付けされたオブジェクトを構築してください。 部分的に初期化された状態やアサーションを避けられる場合は、オブジェクト式を優先してください。より明確で型の不変条件を維持できる場合は、段階的な構築でも問題ありません。
metabase-libの不透明型はブラックボックスとして扱ってください。Lib.Queryなどの型を扱うには、その内部表現にキャストするのではなく、metabase-libの関数を使用してください。- 非同期状態とエラー状態を明示的に型付けしてください(判別可能なユニオンまたはデータレイヤーの型付き結果) — ローディング/エラー/空の状態を暗黙のままにしないでください。
関数シグネチャ
- 安定性と明確さが向上する箇所では、公開コントラクトを明示します。 共有される境界では、必要に応じてパラメータと戻り値の型を注釈付けします。ローカルの値は型推論に任せます。宣言に明示的な形状チェックが必要な場合は、
satisfiesまたは注釈を使用します。 - その操作がサポートする入力を受け入れ、最も正確で正直な結果を返します。 関数内部では回避可能な不確実性を絞り込みますが、戻り値の型において意味のある null 許容性とユニオン variant は維持します。
- 位置引数が混同しやすい場合は名前付きオプションを使用します。 同じ型の連続する引数は有用な警告サインですが、明確な API を自動的に書き直す必要があるというわけではありません。
- 制御フローやエラーハンドリングが明確になる場合は
async/awaitを使用します。 既存の Promise を直接返すことも有効です。Promise の戻り値型だけではasyncを追加する必要はありません。
Null と undefined
- ソースで絞り込みます。値がある corner case でのみオプションである場合、すべてのレイヤーに
undefinedを通すのではなく、生成側でガードします。 - オプションの値には適切なデフォルトを。消費側では
?.と??を使用します。 - 操作が存在する値を必要とする場合は、null 許容のリスト要素を絞り込みます。 欠落したエントリを破棄すべき場合は型ガードでフィルタリングします。その不在や位置が意味を持つ場合は保持します。
Xがnullになり得ない場合は、非厳密な null 比較(X != null)を避けます — 厳密なチェックを使用するか、型を絞り込みます。必要な場合はcheckNotNullを使用します。- 実際の null 許容性を API の実装と照らし合わせて確認します。API エンドポイントの実装を見つけ、フィールドが実際に null になり得るかどうかを確認します。
命名
- 名前はメカニズムではなくエンティティを記述します。名前は値が何を保持するかを反映しなければなりません。
- ドメインの語彙を使用し、有用な場合は単位を含めます(
timeoutMs、widthPx、temperatureC)。Info、Data、Entityが意味を曖昧にする場合は、より具体的な名前を優先します。ただし、既に明確な確立された用語は維持します。 - 関連する概念を揃えます: 関連する API 全体で動詞の慣例を一貫させます。
- 現在の意味ではなく実装の経緯をエンコードする名前は使用しません。
Base、New、Old、Initialのような接尾辞には、実際の意味上の区別が必要であり、そうでなければ削除します。 - ドメイン値に不可解な識別子(
v、n、$n)は避けます。短い名前は小さな慣例的な文脈(ループインデックスi、座標x/y、ジェネリックパラメータT/K/V)でのみ許容されます。
コードの構造と整理
- 重複よりも再利用を優先します。コードベースには既に多くのユーティリティ関数があります — それらを活用してください。重複したロジックを導入する場合は、共有ユーティリティに抽出します。
- 汎用ヘルパーは機能フォルダに属しません。共有ユーティリティに昇格させます。
- 関数は小さく単一目的に保ちます。100 行を超える関数はレビューが困難です — それぞれが単一の責務と最小限の依存関係を持つ、焦点を絞った名前付きヘルパーに分割します。必要に応じて、ユニットテストでカバーします。
- 独立した複雑な JSX は名前付きコンポーネントに抽出します。同一ファイルか別ファイルかは、再利用性、結合度、テスト容易性、可読性で判断します。
コメント
- デフォルトではコメントを付けません。適切に名付けられた識別子が
whatを伝えます。 - コメントは簡潔にします。
whyが自明でない場合にのみ、短く簡潔なコメントを追加します。たとえば、ワークアラウンド、隠れた不変条件、微妙な順序の制約、巧妙な簡略化などです。実際の実装を文書化することは決してせず、意図と why に焦点を当てます。 - JSDoc は実装ではなくコントラクトを記述します。エクスポートされた関数、フック、コンポーネントでは、呼び出し側が何を得るかを伝えます。すなわち、何をするか、何を返すか、エッジケースで何が起こるか(例: 読み込み中に
undefined)です。
テストの作成
テストは、内部実装ではなく、期待されるユーザーから見える振る舞い、または関数やコンポーネントの公開コントラクトを検証すべきです。そのコントラクトを維持するリファクタリングは、テストを壊すべきではありません。
新規または編集されたテスト(Jest と Cypress を含む)については:
- 期待される契約を特定します。 要件から始めます。ユーザーまたは呼び出し元が何を観察すべきでしょうか?セットアップの前提条件と実行パスを理解するために該当するソースを読みますが、現在の実装を正しい動作の定義として扱うためではありません。
- 現実的なフィクスチャを用意します。 テスト対象の動作に到達するために必要な props、state、モックデータ、プロバイダーを提供します。モックされたエンドポイント内のデータは、コンポーネントの prop に対する条件を満たしません。
- カバレッジを計画します。 各ケースの前提条件、アクション、観察可能な結果を特定します。複雑なシナリオでは、短いケースマップがカバレッジの整理に役立ちます。
- 以下のルールに従ってテストを記述します。
- 完了前の検証を使用して、完成した変更をレビューします。 各テストが主張する動作を実際に検証しているかどうかも含めます。
- 結果を要約します。 追加したカバレッジと、未解決の問題や実行されなかったチェックを簡潔に説明します。ユーザーが要求した回答形式に従います。
- フロントエンドガイドのセットアップパターンを使用します。 新規作成、移動、その他の編集を行ったパラメーター化されたユニット/コンポーネントのセットアップ関数は、位置指定の選択やブール値ではなく、
SetupOptsとして型付けられた名前付きオプションを受け取ります。 - モックは実際の契約に対して型付けします。 モックされたエクスポートには、関数をインポートして
jest.mockedを使用することを優先します。jest.requireMockを使用する場合は、モックがそのエクスポート契約を保持している場合に限り、typeof import("module-path")を指定します。jest.fnのコールバックには、実際のコールバックに一致する型付けされた実装、または明示的な引数/戻り値の型を与えます。素のjest.fn()はデフォルトでanyになります。 - フィクスチャは所有する契約に対して型付けします。 モックデータと遅延 Promise には正規のレスポンス型を使用します。フィクスチャ自体から型を導出しても、API との互換性は確立されません。解析またはインターセプトされたリクエスト/レスポンスボディに境界ルールを適用します。型付けされていない結果は、アサーション内も含め、使用する前に
unknownとして取得します。unknownの値を独立した期待値オブジェクトと比較する場合、それを絞り込む必要はありません。 - テストインフラストラクチャを再利用します。 プロバイダー、サーバーモック、非同期ヘルパーを追加する前に、共有のテストサポートを検索します。エンドポイントには共有のサーバーモックヘルパーを使用し、適切な場合は制御された非同期完了のために
metabase/utils/promiseのdefer<T>()を使用します。共有ヘルパーを拡張する際は、他の呼び出し元を保持します。 - テストが主張する内容を実際に検証します。 繰り返しをテストするにはアクションを繰り返します。保留中の動作や順序を確認する場合は、非同期完了を制御し、すべてが完了した後だけでなく、関連する中間状態でアサートします。期待される効果と、契約に関連する望ましくない効果の不在を確認します。呼び出し回数が重要な場合は、引数と同様に回数もアサートします。
toHaveBeenCalledWithは追加の呼び出しを許可します。リトライをテストする前に、失敗が観察可能になるまで待ちます。 - 観察可能な結果をアサートします。 契約によって要求される、レンダリングされた動作、戻り値、または外部から観察可能な効果を確認します。コールバックの引数、呼び出し回数、順序は、それらが契約の一部である場合にのみアサートし、内部ヘルパーの呼び出しやコンポーネントの state を反映するためではありません。近くのアサーションを盲目的にコピーするのではなく、該当するテスト規約を再利用します。
- アサーションに異議を唱えます。 期待される動作が欠けている、または誤っている場合、このテストは失敗するでしょうか?複数ステップのインタラクションでは、最終状態だけでなく、ステップ間の観察可能な遷移を検証します。
- テストを修復する際は、意図されたカバレッジを保持します。 曖昧なセレクターや欠陥のあるセットアップを修正する際に、テストが検証することを意図していた動作を落とさないようにします。アサーション自体が期待される契約を誤って述べている場合は、要件に照らして修正します。現在の実装に合わせるためだけに弱めないでください。
完了前に検証する
- 完了したらプロジェクトの型チェックを実行してください(上記の共有 TypeScript コマンドを参照)。
- 完成した変更をレビューしてください。
typescript-reviewを使用して、該当するガイドラインに照らして、テストやヘルパーを含む完全な差分と新規ファイルを確認してください。テスト、lint、型チェックを通過しただけでは、ガイドラインに準拠していることにはなりません。
コメント
Markdown 対応