hide-unsafe-assertions について
公開関数のシグネチャを適切な型に保ちながら、安全でない型アサーションを実装内に閉じ込めるTypeScriptスキルです。アサーションの検証、理由の記録、テスト方法を開発者に示します。
主な機能
- 01公開シグネチャの型を維持
- 02実装内にアサーションを限定
- 03アサーション前に値を検証
- 04安全な理由を記録
- 05アサーションを含む関数のテスト
活用シーン
- 01型定義の弱いライブラリのラップ
- 02API関数に明確な戻り値型を公開
- 03TypeScriptが推論できないケースへの対応
- 04呼び出し元へのアサーション拡散の防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill hide-unsafe-assertionsインストール後、Claude Code で/hide-unsafe-assertionsを入力して読み込まれたか確認します。
詳細
型安全な関数の中に危険な型アサーションを隠す
概要
型シグネチャはクリーンに保ち、実装の中にアサーションを隠します。
関数の内部で型アサーションやanyが必要になるのは問題ありません。公開シグネチャが正しければ大丈夫です。ユーザーには型安全なAPIを提供し、危険なコードは内部に閉じ込めます。
このスキルを使う場面
- 関数の実装で型アサーションが必要な場合
- TypeScriptがロジックを追跡できない場合
- 型定義が不十分なライブラリをラップする場合
- 内部の複雑さを隠し、外部APIをクリーンに保つ場合
絶対に守るルール
Never compromise type signatures for implementation convenience.
Hide assertions inside well-typed functions.
覚えておきましょう:
- 型シグネチャはAPIの契約です
- 実装は隠れた詳細です
- ユーザーにアサーションを見せてはいけません
- アサーションを多用したコードは十分にテストしましょう
検出:公開されている危険な型
// Bad: unsafe return type exposes any
async function fetchPeak(peakId: string): Promise<unknown> {
return checkedFetchJSON(`/api/peaks/${peakId}`);
}
// Every caller must assert:
const peak = await fetchPeak('denali') as MountainPeak; // Tedious!
より良い方法:アサーションを隠す
async function fetchPeak(peakId: string): Promise<MountainPeak> {
return checkedFetchJSON(`/api/peaks/${peakId}`) as Promise<MountainPeak>;
}
// Callers get clean types:
const peak = await fetchPeak('denali');
// ^? MountainPeak
アサーションは内部に隠され、呼び出し側にはクリーンなAPIが提供されます。
隠したアサーションの内部で検証する
async function fetchPeak(peakId: string): Promise<MountainPeak> {
const maybePeak = await checkedFetchJSON(`/api/peaks/${peakId}`);
// Validation adds safety to the assertion
if (!maybePeak || typeof maybePeak !== 'object' || !('name' in maybePeak)) {
throw new Error(`Invalid peak data: ${JSON.stringify(maybePeak)}`);
}
return maybePeak as MountainPeak;
}
これで、アサーションを実行時にも検証できます。
TypeScriptが追跡できない場合
function shallowEqual(a: object, b: object): boolean {
for (const [k, aVal] of Object.entries(a)) {
if (!(k in b) || aVal !== b[k]) {
// ~~~~
// Element implicitly has an 'any' type
return false;
}
}
return Object.keys(a).length === Object.keys(b).length;
}
k in bが真であることは分かっていますが、TypeScriptはこれをb[k]と結び付けて認識しません。
間違った修正:シグネチャを弱める
// DON'T: weakening b to any exposes unsafety
function shallowEqual(a: object, b: any): boolean {
// ...
}
shallowEqual({x: 1}, null); // No error! Crashes at runtime.
正しい修正:アサーションを隠す
function shallowEqual(a: object, b: object): boolean {
for (const [k, aVal] of Object.entries(a)) {
// Hidden assertion - we've checked k in b
if (!(k in b) || aVal !== (b as any)[k]) {
return false;
}
}
return Object.keys(a).length === Object.keys(b).length;
}
shallowEqual({x: 1}, null);
// ~~~~
// Argument of type 'null' is not assignable to parameter of type 'object'.
型シグネチャはクリーンなままで、アサーションの適用範囲も限定されます。
隠れたアサーションとしての関数オーバーロード
// Overload presents clean signature to callers
async function fetchPeak(peakId: string): Promise<MountainPeak>;
async function fetchPeak(peakId: string): Promise<unknown> {
return checkedFetchJSON(`/api/peaks/${peakId}`);
}
const denali = fetchPeak('denali');
// ^? Promise<MountainPeak>
実装はunknownを返しますが、呼び出し側にはMountainPeakが返されるように見えます。
アサーションの適用範囲を限定する
// DON'T: Assertion on whole object
const config: Config = {
a: 1,
b: 2,
c: { key: value }
} as any; // No checking on a, b!
// DO: Assertion only on problem area
const config: Config = {
a: 1,
b: 2, // These are still checked
c: { key: value as any } // Only this is unsafe
};
アサーションが妥当な理由を説明する
function shallowEqual(a: object, b: object): boolean {
for (const [k, aVal] of Object.entries(a)) {
// `(b as any)[k]` is safe because we've verified `k in b`
if (!(k in b) || aVal !== (b as any)[k]) {
return false;
}
}
return Object.keys(a).length === Object.keys(b).length;
}
コメントがあれば、将来の保守担当者もアサーションの理由を理解できます。
十分にテストする
アサーションを隠した関数は、より入念なテストが必要です。
describe('fetchPeak', () => {
it('handles valid peak data', async () => {
const peak = await fetchPeak('denali');
expect(peak.name).toBe('Denali');
expect(peak.elevationMeters).toBe(6190);
});
it('throws on invalid data', async () => {
mockFetch({ invalid: 'data' });
await expect(fetchPeak('unknown')).rejects.toThrow('Invalid peak');
});
it('handles missing fields', async () => {
mockFetch({ name: 'Partial' }); // Missing fields
await expect(fetchPeak('partial')).rejects.toThrow();
});
});
圧力に負けないための指針
1. 「戻り値の型を変えるだけでいい」
圧力:「アサーションを避けるために、戻り値をunknownにしましょう」
**応答:**その方法では、すべての呼び出し側に危険な処理が押し付けられます。
**対応:**クリーンなシグネチャを保ち、実装の中にアサーションを隠します。
2. 「アサーションは危険だ」
圧力:「アサーションは一切使わないようにしましょう」
**応答:**必要になる場合もあります。適切に閉じ込めてテストすれば問題ありません。
**対応:**隠し、検証し、理由を説明して、テストします。
危険信号 — 作業を止めて再検討する
- 関数シグネチャに便宜上
anyやunknownを含めている - 呼び出し側のコードにアサーションが散らばっている
- 実装エラーを避けるためにシグネチャを変更している
- アサーションを多用したコードがテストされていない
よくある正当化(すべて誤り)
| 言い訳 | 実情 |
|---|---|
| 「そのほうが正直です」 | 呼び出し側に危険な処理を押し付けるほうが問題です |
| 「アサーションは悪いものです」 | 適切に閉じ込めたアサーションなら問題ありません |
| 「ユーザー側で型を絞り込めます」 | ユーザーにその作業を求めるべきではありません |
クイックリファレンス
// DON'T: Expose unsafety in signature
function fetch(): Promise<unknown> { ... }
const data = await fetch() as Data; // Assertion at every call site
// DO: Hide assertion in implementation
function fetch(): Promise<Data> {
return api.fetch() as Promise<Data>; // Hidden, one place
}
// DO: Narrow scope
const obj = { a: value as any }; // Not: whole object as any
// DO: Document and test
// This assertion is valid because... [explanation]
return data as Data;
要点
危険なコードは隠し、クリーンな型を公開します。
型アサーションやany型が必要になる場合もあります。シグネチャではなく、関数の実装の中に置きましょう。妥当である理由を説明し、十分にテストしましょう。
参考資料
Dan Vanderkam著『Effective TypeScript』の項目45「Hide Unsafe Type Assertions in Well-Typed Functions」に基づいています。