structural-typing について
TypeScript の構造的型付け、代入可能性、より厳密なチェックが必要な場面を解説する学習スキルです。柔軟な API、クラス、テスト用オブジェクトの理解に役立ちます。
主な機能
- 01形状に基づく代入可能性の解説
- 02余分なプロパティの挙動の整理
- 03クラスの構造的型付けの説明
- 04ブランド型による厳密なチェック
活用シーン
- 01インターフェースと関数引数の設計
- 02モックライブラリなしでのテスト用オブジェクト作成
- 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:「構造的型付けに慣れましょう」に基づいています。