mcpField
T
types-as-sets

types-as-sets

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

types-as-sets について

型を値の集合、代入可能性を部分集合の関係として捉えるTypeScript学習スキルです。この見方を使い、ユニオン、インターセクション、`extends`、`never`、`unknown`を説明します。

主な機能

  1. 01型を値の集合として捉える
  2. 02ユニオンとインターセクションを説明
  3. 03`extends`を部分集合として解釈
  4. 04`never`と`unknown`を解説
  5. 05集合の考え方で代入可能性を分析

活用シーン

  1. 01代入可能性エラーの調査
  2. 02ジェネリック制約の推論
  3. 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 | T2T1 ∪ T2(和集合)どちらかに含まれる値
T1 & T2T1 ∩ 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「型を値の集合として考える」に基づいています。

コメント