mcpField
S
structural-typing

structural-typing

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

structural-typing について

TypeScript の構造的型付け、代入可能性、より厳密なチェックが必要な場面を解説する学習スキルです。柔軟な API、クラス、テスト用オブジェクトの理解に役立ちます。

主な機能

  1. 01形状に基づく代入可能性の解説
  2. 02余分なプロパティの挙動の整理
  3. 03クラスの構造的型付けの説明
  4. 04ブランド型による厳密なチェック

活用シーン

  1. 01インターフェースと関数引数の設計
  2. 02モックライブラリなしでのテスト用オブジェクト作成
  3. 03予期しない型の代入の調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill structural-typing

インストール後、Claude Code で/structural-typingを入力して読み込まれたか確認します。

詳細

構造的型付けに慣れましょう

概要

TypeScript は構造的型付けを使用します。形が合っていれば、型に適合します。

型を明示的に宣言する必要がある名目的型付けとは異なり、TypeScript は構造を確認します。この仕組みを理解しておくと、予想外の動作を防ぎ、強力なパターンを活用できます。

このスキルを使う場面

  • TypeScript が「誤った」値を受け入れて驚いたとき
  • インターフェースや関数のパラメーターを設計するとき
  • モックオブジェクトを使ってユニットテストを書くとき
  • 「起こるはずのない」型エラーをデバッグするとき
  • 余分なプロパティが許可される理由を理解したいとき

鉄則

NEVER assume types are "sealed" - they always allow extra properties.

次のことを受け入れてください:

  • 必要なプロパティがあれば、代入できます
  • 余分なプロパティがあっても、値は無効になりません
  • クラスは同一性ではなく、構造で比較されます

検出:「封印された型」という思い込み

TypeScript が値を受け入れて驚いた場合、名目的型付けを前提にしている可能性があります。

interface Vector2D {
  x: number;
  y: number;
}

function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x ** 2 + v.y ** 2);
}

// ✅ Works as expected
calculateLength({ x: 3, y: 4 });  // 5

// ✅ Also works! Has x and y, so it's a valid Vector2D
const namedVector = { x: 3, y: 4, name: 'Pythagoras' };
calculateLength(namedVector);  // 5

// ✅ Even 3D vectors work (but give wrong results!)
const vector3D = { x: 3, y: 4, z: 5 };
calculateLength(vector3D);  // 5 (ignores z!)

構造的型付けの原則

必要なプロパティを、互換性のある型で少なくとも備えていれば、その値を型に代入できます。

interface Point {
  x: number;
  y: number;
}

// All of these are valid Points:
const p1: Point = { x: 1, y: 2 };                    // Exact match
const p2: Point = { x: 1, y: 2, z: 3 };             // Extra property (via variable)
const p3: Point = { x: 1, y: 2, name: 'origin' };   // Different extra property

// But not this:
const p4: Point = { x: 1 };  // Error: missing 'y'

関数で重要な理由

interface Vector2D { x: number; y: number; }
interface Vector3D { x: number; y: number; z: number; }

function normalize(v: Vector3D) {
  const length = Math.sqrt(v.x ** 2 + v.y ** 2 + v.z ** 2);
  return {
    x: v.x / length,
    y: v.y / length,
    z: v.z / length,
  };
}

// This is a bug, but TypeScript doesn't catch it:
function calculateLength2D(v: Vector2D) {
  return Math.sqrt(v.x ** 2 + v.y ** 2);
}

// normalize calls calculateLength2D internally
function normalize(v: Vector3D) {
  const length = calculateLength2D(v);  // Bug: ignores z!
  // Vector3D is assignable to Vector2D
}

クラスでの構造的型付け

class SmallContainer {
  num: number;
  constructor(num: number) {
    if (num < 0 || num >= 10) {
      throw new Error('Must be 0-9');
    }
    this.num = num;
  }
}

const a = new SmallContainer(5);  // OK

// This also type-checks, but bypasses validation!
const b: SmallContainer = { num: 2024 };  // No error!

// Because SmallContainer structurally is just { num: number }

メリット:簡単なテスト

構造的型付けを使うと、モックライブラリが不要になり、テストが簡単になります:

interface Database {
  runQuery(sql: string): any[];
}

function getUsers(db: Database) {
  return db.runQuery('SELECT * FROM users');
}

// In tests, just create an object with the right shape:
test('getUsers', () => {
  const mockDb = {
    runQuery(sql: string) {
      return [{ name: 'Alice' }, { name: 'Bob' }];
    }
  };
  
  const users = getUsers(mockDb);  // Works! No type error
  expect(users).toHaveLength(2);
});

「余剰プロパティチェック」の例外

オブジェクトリテラルは特別に扱われ、TypeScript は余分なプロパティにフラグを立てます:

interface Point { x: number; y: number; }

// Extra property in object literal: Error!
const p: Point = { x: 1, y: 2, z: 3 };
//                            ~ Object literal may only specify known properties

// But via intermediate variable: No error
const temp = { x: 1, y: 2, z: 3 };
const p: Point = temp;  // OK

これは使いやすさのための機能であり、構造的型付けのルールが変わるわけではありません。詳しくは excess-property-checking スキルをご覧ください。

構造的型付けで問題が起きる場面

問題:ベクトルの次元が不正
// Solution 1: Use optional never to forbid property
interface Vector2D {
  x: number;
  y: number;
  z?: never;  // Explicitly disallows z
}

// Solution 2: Use branded types (see branded-types skill)
type Vector2D = { x: number; y: number } & { _brand: 'Vector2D' };
問題:クラスの検証が回避される
// Solution: Make the class have unique properties
class SmallContainer {
  private readonly _brand = 'SmallContainer';  // Can't be faked
  num: number;
  // ...
}

プレッシャーへの対処プロトコル

1.「これは許可されるべきではない」

プレッシャー:「TypeScript は余分なプロパティを持つ値を拒否すべきです」

**回答:**それは名目的型付けです。TypeScript は構造的型付けを使用します。

**対処:**より厳密なチェックが必要な場合は、ブランド型などの手法を使います。

2.「自分のクラスは特別なはず」

プレッシャー:「自分のクラスの本物のインスタンスだけが有効なはずです」

**回答:**クラスは構造的に型付けされます。区別するには、プライベートフィールドを追加します。

**対処:**名目的型付けに近い動作が必要な場合は、プライベートフィールドやブランドを使います。

危険信号 — 中断して再検討してください

  • 余分なプロパティが値を無効にすると考えている
  • クラスの同一性が重要だと期待している
  • TypeScript が「誤った」値を受け入れて驚いている
  • 型は「封印されている」と考えている
  • TypeScript が認識しない検証ロジックがある

よくある正当化(すべて誤り)

言い訳実際
「正しい型ではない」形が合っていれば、正しい型です。
「自分のクラスは検証する」構造的に互換性のあるオブジェクトはコンストラクターを回避できます。
「余分なプロパティは機能すべきではない」TypeScript では機能します。

クイックリファレンス

場面構造的型付けの動作
値に余分なプロパティがある許可される(オブジェクトリテラルを除く)
クラスのインスタンスクラスの同一性ではなく、構造で比較される
関数のパラメーター構造的に互換性のある値なら使用できる
オブジェクトリテラルの代入余剰プロパティにフラグが立つ(特別なケース)

要点

TypeScript が確認するのは同一性ではなく、形です。

必要なプロパティをすべて互換性のある型で備えていれば、その値を代入できます。これによりテストが簡単になり、柔軟な API を実現できますが、予想外の動作につながることもあります。より厳密なチェックが必要な場合は、ブランド型などの手法を使いましょう。

参考文献

Dan Vanderkam 著『Effective TypeScript』項目 4:「構造的型付けに慣れましょう」に基づいています。

コメント