types-as-sets について
型を値の集合、代入可能性を部分集合の関係として捉えるTypeScript学習スキルです。この見方を使い、ユニオン、インターセクション、`extends`、`never`、`unknown`を説明します。
主な機能
- 01型を値の集合として捉える
- 02ユニオンとインターセクションを説明
- 03`extends`を部分集合として解釈
- 04`never`と`unknown`を解説
- 05集合の考え方で代入可能性を分析
活用シーン
- 01代入可能性エラーの調査
- 02ジェネリック制約の推論
- 03条件型の理解
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill types-as-setsインストール後、Claude Code で/types-as-setsを入力して読み込まれたか確認します。
詳細
型を値の集合として考える
概要
型とは、可能な値の集合です。代入可能性は部分集合を意味します。
型を集合として理解すると、ユニオン、インターセクション、extends、neverについて考えやすくなります。このメンタルモデルを使うと、TypeScriptの挙動を直感的に理解できます。
このスキルを使う場面
A & BがAやBよりも多くのプロパティを持つ理由がわからないextendsが「部分集合」を意味する理由がわからない- ユニオン型やインターセクション型について考えるとき
never型やunknown型を扱うとき- 「代入できない」エラーをデバッグするとき
絶対的なルール
ALWAYS think "subset" when you see "extends" or "assignable to".
覚えておきましょう:
- ユニオン(
|)=より大きな集合(定義域の和集合) - インターセクション(
&)=より小さな集合(定義域の積集合) extends=「部分集合である」never=空集合(値がない)unknown=普遍集合(すべての値)
判別:代入可能性の混乱
代入可能性のエラーに戸惑ったら、集合として考えてみましょう。
type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;
const ab: AB = 'A'; // OK: 'A' is in {'A', 'B'}
const ab12: AB12 = ab; // OK: {'A', 'B'} ⊆ {'A', 'B', 12}
declare let twelve: AB12;
const back: AB = twelve; // Error!
// {'A', 'B', 12} is NOT a subset of {'A', 'B'}
集合論のメンタルモデル
TypeScriptから集合論への対応
| TypeScript | 集合論 | 意味 |
|---|---|---|
never | ∅(空集合) | 値がない |
リテラル型 "A" | 単一要素 {A} | 1つの値 |
T1 | T2 | T1 ∪ T2(和集合) | どちらかに含まれる値 |
T1 & T2 | T1 ∩ T2(積集合) | 両方に含まれる値 |
unknown | 普遍集合 | すべての値 |
extends | ⊆(部分集合) | 「含まれている」 |
| 「代入可能」 | ⊆(部分集合) | 「含まれている」 |
インターセクション&でプロパティが増える理由
最初は直感に反するように思えるかもしれません。
interface Person { name: string; }
interface Lifespan { birth: Date; death?: Date; }
type PersonSpan = Person & Lifespan;
// PersonSpan has MORE properties, not fewer!
const ps: PersonSpan = {
name: 'Alan Turing',
birth: new Date('1912/06/23'),
death: new Date('1954/06/07'),
}; // OK
なぜでしょうか? プロパティではなく、値の集合を交差させているからです。
- Person=
nameプロパティを持つすべてのオブジェクト - Lifespan=
birth(および省略可能なdeath)を持つすべてのオブジェクト - Person & Lifespan=両方のプロパティ群を持つオブジェクト
値の集合は小さくなりますが、個々の値が持つプロパティは増えます。
ユニオン|で保証されるプロパティが減る理由
type K = keyof (Person | Lifespan);
// ^? type K = never
// No keys are guaranteed on BOTH Person AND Lifespan
値の集合は大きくなりますが、利用できるプロパティは少なくなります。
extendsキーワード
TypeScriptでは、extendsは「部分集合である」という意味です。
interface Vector1D { x: number; }
interface Vector2D extends Vector1D { y: number; }
interface Vector3D extends Vector2D { z: number; }
// Vector3D ⊆ Vector2D ⊆ Vector1D (as sets of values)
// A Vector3D IS-A Vector2D IS-A Vector1D
これはジェネリックの制約にも当てはまります。
function getKey<K extends string>(val: any, key: K) { /* ... */ }
// K can be any subset of string:
getKey({}, 'x'); // OK: 'x' ⊆ string
getKey({}, Math.random() < 0.5 ? 'a' : 'b'); // OK: 'a'|'b' ⊆ string
getKey({}, 12); // Error: number ⊄ string
never型(空集合)
neverは空集合で、値を含みません。
const x: never = 12;
// ~ Type 'number' is not assignable to type 'never'.
// never is useful for exhaustiveness checking
function assertNever(x: never): never {
throw new Error('Unexpected value: ' + x);
}
unknown型(普遍集合)
unknownは普遍集合で、すべての値を代入できます。
const x: unknown = 'hello'; // OK
const y: unknown = 42; // OK
const z: unknown = null; // OK
// But you can't use unknown without narrowing
const str: string = x; // Error: must narrow first
配列とタプル
const list = [1, 2];
// ^? const list: number[]
const tuple: [number, number] = list;
// Error! number[] is not assignable to [number, number]
// Because: there exist number[] that aren't pairs ([], [1], [1,2,3])
集合number[]は[number, number]の部分集合ではありません。
実践的な応用
ジェネリック制約を理解する
// T must be a subset of objects with an 'id' property
function getById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
条件型を理解する
// "If A is a subset of B, then X, else Y"
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true ('hello' ⊆ string)
type B = IsString<number>; // false (number ⊄ string)
圧力に対抗するためのプロトコル
1. 「インターセクションならプロパティが減るはず」
圧力:「A & Bには、両方に共通するプロパティがあるはずです」
**応答:**交差させているのはプロパティではなく、値の集合です。
対応:「両方のインターフェースを満たすオブジェクトは何か?」と考えましょう。
2. 「extendsは継承を意味する」
圧力:「extendsはクラスの継承に関するものです」
**応答:**型において、extendsは「部分集合である」という意味です。
**対応:**読み進める際に「extends」を「部分集合である」に置き換えましょう。
危険信号 — 立ち止まって考え直す
&がプロパティを取り除くと考えている|がプロパティを追加すると考えているextendsを古典的な継承と混同している- オブジェクト型は「開いている」(追加のプロパティを許容する)ことを忘れている
よくある正当化(すべて誤り)
| 言い訳 | 実際 |
|---|---|
| 「インターセクションは共通部分を意味する」 | プロパティではなく、値に関するものです |
| 「extendsは継承を意味する」 | 型では「部分集合である」という意味です |
| 「ユニオンにはすべてのプロパティがある」 | ユニオンで保証されるのは共通のプロパティだけです |
クイックリファレンス
| 型の式 | 集合としての解釈 | サイズ |
|---|---|---|
never | 空集合 | 0個の値 |
"A" | 単集合 | 1個の値 |
"A" | "B" | 和集合 | 2個の値 |
string | すべての文字列 | ∞個の値 |
unknown | 普遍集合 | すべての値 |
A & B | 積集合 | 通常はより小さい |
A | B | 和集合 | 通常はより大きい |
要点
型は値の集合です。代入可能性は、部分集合かどうかのチェックです。
これを理解すると、TypeScript の動作が直感的に分かるようになります。extends は「〜の部分集合」を意味し、& は値の集合を交差させ(必要なプロパティが増えます)、| は値の集合を結合します(保証されるプロパティが減ります)。
参考資料
Dan Vanderkam 著『Effective TypeScript』項目 7「型を値の集合として考える」に基づいています。