mcpField
H
hide-unsafe-assertions

hide-unsafe-assertions

github.com/marius-townhouse開発645 回インストール39 週間前に更新
リポジトリ

hide-unsafe-assertions について

公開関数のシグネチャを適切な型に保ちながら、安全でない型アサーションを実装内に閉じ込めるTypeScriptスキルです。アサーションの検証、理由の記録、テスト方法を開発者に示します。

主な機能

  1. 01公開シグネチャの型を維持
  2. 02実装内にアサーションを限定
  3. 03アサーション前に値を検証
  4. 04安全な理由を記録
  5. 05アサーションを含む関数のテスト

活用シーン

  1. 01型定義の弱いライブラリのラップ
  2. 02API関数に明確な戻り値型を公開
  3. 03TypeScriptが推論できないケースへの対応
  4. 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」に基づいています。

コメント