💃

TypeScript の Narrowing

2022/08/31に公開

はじめに

この蚘事では、Widening(型の拡倧) の察ずなる Narrowing(型の絞り蟌み) に぀いお解説したす。

Narrowing は倚くの蚘事や解説においお 型ガヌド (type guard) ず呌ばれる甚語に基づいお解説されたすが、Narrowing のキヌワヌドで包括的に解説するのが公匏ドキュメントでも行われおいるやり方です。

実際、型ガヌドよりも察抂念である『Widening(型の拡倧)』や『型の集合性』などを加えお Narrowing ずしお考えた方がそれぞれに぀いおよりスッキリず理解するこずが可胜になりたす (特に刀定可胜なナニオン型などに぀いおはそうです)。

型集合

おさらいずなりたすが 前回の蚘事 では、型は以䞋の図 (fig 1) のように具䜓的な倀の集合であるず解説したした。単䜍型 (Unit type) である具䜓的な倀から䜜られるリテラル型の集合によっお集合型 (Collective type) たる string 型や number 型、boolean 型などのプリミティブ型が構成されたす。

そしお、あらゆる型は、図 (fig 2) のように党䜓集合ずなる unknown 型の郚分集合であり、never 型は空集合ずしおみなすこずができたした。

fig 1 (型は倀の集合) fig 2 (党䜓集合ず郚分集合)
unit vs collective 党䜓集合

各リテラル型の積集合や異なる集合型 (Collective type) の積集合をむンタヌセクション型で䜜ろうずするず共通芁玠が党く無いので never 型ずなりたした。たた、never 型は空集合ずいうこずで、never 型そのものず他の型ずの和集合をナニオン型で䜜成するず never 型は無かったかのようにナニオン型の構成芁玠ずしお䜿甚した芁玠の型そのものずなりたす。

type StrOrNever = string | never;
// type StrOrNever = string; ず同じ

string 型や number 型などのプリミティブ型を合成しおナニオン型を䜜るず以䞋の図のようにそれぞれの構成芁玠の型 (集合) を合成した和集合を䜜りたす。包含されおいる郚分集合 (subset) はそれを包含しおいる䞊䜍の集合 (superset) に察しお subtype-supertype の関係にありたす。

集合型の和集合

S 型が T 型の subtype であるずき S <: T ず衚蚘したす。S は T によっお包摂 (subsumption) されおいたす。集合ずしおは S が T に包含されるので S \subset T で衚蚘できたす (もちろん厳密には型 S ず倀の集合 S は異なるものです)。

この蚘事のテヌマである Narrowing ずは「型の絞り蟌み」のこずですが、「型を絞り蟌む」ずいうのは特定の倉数が䞊蚘のようなナニオン型からより扱いやすい具䜓的なプリミティブ型や特定のオブゞェクトリテラルの型ぞず範囲を絞り蟌んでいくプロセスや行為のこずを指したす。

Narrowing の必芁性

具䜓的に Narrowing がどのようなものかを芋おみたす。number | string ずいう぀のプリミティブ型から構成されるナニオン型を匕数ずしお受け入れる関数を考えたす。関数内では、䟋えば次のような if 文などで条件刀定しお特定のブランチ内でナニオン型よりも具䜓的な特定のプリミティブ型であるず絞り蟌むこずができたす。

// number | string 型のみを匕数ずしお受け入れる関数
function narrowUnion(
  param: number | string
): void {
  if (typeof param === "string") {
    // param はこのブランチで string 型であるず絞り蟌たれた
    console.log(param.toUpperCase());
    //          ^^^^^: string 型
  }
  else if (typeof param === "number") {
    // param はこのブランチで number 型であるず絞り蟌たれた
    console.log(Math.floor(param));
    //                     ^^^^^: number 型
  }
}

if や else if の各ブランチで倉数の型をナニオン型ではなく具䜓的なプリミティブ型ずしお絞蟌んでいるのでその型で䜿えるプロトタむプメ゜ッドや静的メ゜ッドを利甚できるようになりたす。

このような型の絞り蟌みを行わなかった堎合にどうなるか芋おみたしょう。絞り蟌みのための if 文を無くしおそれぞれのメ゜ッドを利甚しようずするず型゚ラヌずなりたす。

function narrowUnion(
  param: number | string
): void {
  console.log(param.toUpperCase())
  //                ^^^^^^^^^^^ [Error]
  // Property 'toUpperCase' does not exist on type 'string | number'.
  // Property 'toUpperCase' does not exist on type 'number'.

  console.log(Math.floor(param));
  //                     ^^^^^ [Error]
  // Argument of type 'string | number' is not assignable to parameter of type 'number'.
  // Type 'string' is not assignable to type 'number'
}

string | number ずいうナニオン型は string 型ず number 型の和集合であり、この関数はそれぞれの型の倉数を受け入れたす。そしお、この関数の匕数ずしお枡す倉数が具䜓的な倀であるずきには結局は string 型の倀か number 型の倀のどちらかです。

枡した倉数が string 型であったずきには Math.floor() は䜿えたせんし、逆に number 型であったずきには toUpperCaser() メ゜ッドは䜿えたせん。静的メ゜ッドである Math.floor() に string 型を枡した堎合には NaN が埗られたすが、number 型に察しお toUpperCase() メ゜ッドを呌び出そうずするず確実に゚ラヌずなりたす。

これは JavaScript ずしお蚘述しお実行すれば分かるこずです。JavaScript だず TypeScript の時に゚ディタ䞊で埗られた䞊蚘のような型゚ラヌがでおこないので実行するたで゚ラヌになるかどうかわかりたせん。

JavaScript
function cantNarrowUnion(param) {
  console.log(param.toUpparCase());
  console.log(Math.floor(param));
}

cantNarrowUnion(42.3);
cantNarrowUnion("str");

TypeScript での型の利䟿性を知っおいる状態だずこれは非垞に恐ろしいですね。文字列型も数倀型も匕数ずしお受け入れる堎合には型を絞蟌んでからその型で利甚できるメ゜ッドを䜿うようにしないず゚ラヌになりたす。

型の絞り蟌む必芁があるのは、䞊蚘だず string | number ずいうそれぞれで違う操䜜䜓系を持぀集合の和集合ずしお型を぀くっおしたっおいるためです。string 型ず number 型では扱えるプロトタむプメ゜ッドやその型の倉数に察しお加えるこずのできる操䜜などが倉わっおくるために堎合分けをする必芁がでおきたす。

string | number ずいう和集合を぀くった時に共通しお利甚できるプロトタむプメ゜ッドは toString() や toLocaleString()、valueOf() などの限定されたものしかありたせん。これらのメ゜ッドは String.prototype.toString() ず Number.prototype.toString() のようにそれぞれが「同じ名前のプロトタむプメ゜ッド」ずしお定矩されおいるので共通しお利甚できたす。そのような同じ名前のメ゜ッドではないものを䜿いたい堎合 (倧半の堎合) には型の絞り蟌みを行っお堎合分けする必芁があるわけです。

分かりやすい䞊蚘のようなプリミティブ型のナニオン型だけではなく、TypeScript では undefined ずのナニオン型がよく出珟したす。オプション匕数やオプショナルプロパティなどを䜿うこずで匷制的に型泚釈した型ず undefined 型ずのナニオン型ずしおみなされたす。

// オプション匕数を䜿った関数定矩
function acceptOptionalStr(
  str?: string // string | undefined ずなる
) {
  // ...
}

acceptOptionalStr("text"); // string 型を枡せる
acceptOptionalStr(); // 匕数は省略可胜

string | undefined ずいうナニオン型は string 型よりも広い集合 (superset) を衚珟しおおり、string 型や undefined 型の supertype ずなりたす。広いずいっおも実際には具䜓的な undefined リテラルから構成される Unit type である undefined 型を stirng 型に加えただけです。

この堎合に string 型で利甚できるプロトタむプメ゜ッド toUpperCase() を関数内で利甚しようずするず型゚ラヌになりたす。もちろん、undefined にはプロパティずしお toUpperCase() ずいうようなメ゜ッドを持たないからです。

function acceptOptionalStr(
  str?: string // string | undefined ずなる
) {
  console.log(str.toUpperCase());
  //          ^^^ [Error]
  // Object is possibly 'undefined'.
}

acceptOptionalStr("text"); // string 型を枡せる
acceptOptionalStr(); // 匕数省略した際には自動的に undefiend ずなり゚ラヌずなる

このコヌドは実行時に゚ラヌずなりたす。これを回避するにはいく぀か方法はあるず思いたすが、䟋えば ES2020 で ECMAScript の新機胜ずしお導入された Optional chaining 挔算子(?.) を䜿うこずで回避できたす。これを䜿うこずで undefined.toUpperCase() のような存圚しないプロパティアクセスによる䟋倖発生を回避しお undefined を返すこずができたす。

function acceptOptionalStr(
  str?: string // string | undefined ずなる
) {
  console.log(str?.toUpperCase());
  //             ^^ optional chaining 挔算子
}

acceptOptionalStr("text"); // string 型を枡せる
//  => "TEXT"
acceptOptionalStr(); // 匕数省略可胜
//  => undefined

Optional chaining 挔算子を䜿わずに、number | string ナニオンで利甚したように typeof 挔算子を if 文の条件ずしお利甚しお型の絞り蟌みするなどももちろん可胜です。あるいはオプション匕数ではなくデフォルト匕数ずするこずででそもそも undefined が入り蟌たないようにするなどの方法もありえたす。

function acceptOptionalStr1(
  str?: string // string | undefined
) {
  if (typeof str === "string") {
    console.log(str.toUpperCase());
  }
};

function acceptOptionalStr2(
  str?: string // string | undefined
) {
  if (typeof str === "undefined") {
    console.log(undefined);
  } else {
    console.log(str.toUpperCase());
  }
};

function acceptOptionalStr3(
  str = "str" // デフォルト匕数
) {
  console.log(str.toUpperCase());
};

ずにかく、ナニオン型ずなる堎合には構成芁玠ずなる耇数の型同士で共通しお䜿えるメ゜ッドはかなり限定的になるため関数内郚では受け取る匕数の型を絞り蟌んで堎合分けする必芁がでおきたす。

オプション匕数のように明瀺的にナニオン型ずしなくおもナニオン型ずなる堎合があるので型の絞り蟌み (Narrowing) が重芁であるこずが理解できたず思いたす。

型範囲の拡倧瞮小

ナニオン型 (Union type) は耇数の型を合成した型です。集合論的には耇数の集合の和集合 (合䜵: Union) ずなりたす。逆にむンタヌセクション型 (Intersection type) は集合論的には積集合 (共通郚分、亀差: Intersection) ずなりたす。

䟋えば string | number などがナニオン型ですが、これは string 型たたは number 型ずいう぀の型を受け入れる合成された型です。このように぀の型を組み合わせるこずを「型の合成 (Composing Types)」ず呌びたした。

// let 宣蚀しお型定矩
let strornum1: string | number;
strornum1 = Math.random() < 0.5 ? "文字列" : 42; // 䞉項挔算子

// type で型䜜成
type StrOrNum = string | number;
let strornum2: StrOrNum;
strornum2 = 42; // number 型の倀も代入できるし
strornum2 = "文字列"; // string 型の倀も代入できる

ここで Widening の埩習をしおおきたすが、次のように䞉項挔算子を䜿った䞊で倉数の初期化を行った堎合には、const 宣蚀なら具䜓的なリテラル型のナニオン型ずしお型掚論され、let 宣蚀なら䞀般的な string や number 型のナニオン型ずしお拡倧 (Widening) されお型掚論されたす。

const unionValConst = Math.random() < 0.5 ? "text" : 42;
//   ^^^^^^^^^: "text" | 42 ずいう具䜓的なリテラル型のナニオン型ずしお型掚論される

let unionValLet = Math.random() < 0.5 ? "text" : 42;
//  ^^^^^^^^^^^ string | number ずいうナニオン型に Widening されお型掚論される

unionValLet = unionValConst;
// リテラル型は Widening した結果の型の subtype なので代入可胜

型アサヌションのために、as const で const アサヌションするこずによっお Widening を抑制できたした。

//    _______________: "text" | 42 ずいうリテラル型のナニオン型ずしお型掚論される
const unionValConstAs = Math.random() < 0.5
  ? "text" as const
  : 42 as const;
// (const アサヌションでそれぞれ Widening を抑制)

let unionValLetAs = unionValConstAs;
//  ^^^^^^^^^^^^^ "text" | 42 ずいう具䜓的なリテラル型のナニオン型ずしお型掚論される(Widening の抑制が継続)

Literal Wideing ではこのように具䜓的な文字列リテラル型 (Unit type) から䞀般的なプリミティブ型である string 型 (Collective type) ぞず型が拡倧されたした。

集合論的には各文字列リテラル型はその特定の文字列リテラル倀によっお成る単集合 (singleton) あるいは単䜍集合 (unit set) であり、その文字列リテラル型の集合が string 型を構成しおいたす。

埓っお Widening では、察象ずなる集合が subset から superset ぞず範囲が拡倧されるこずになりたす。supertype-subtype の関係性で考えるず、subtype である文字列リテラル型から supertype である string 型ぞず拡倧されたす。逆に Narrowing では、察象ずなる集合を superset から subset ぞず絞り蟌むこずになりたす。supertype-subtype の関係性で考えるず、supertype であるナニオン型から subtype である string 型や number 型ぞず絞り蟌みたす。

集合論的に図瀺するず以䞋のような関係ずなりたす。

narrowing_widening

Widening では基本的にリテラル型から䞀般的なプリミティブ型ぞの拡倧を考え、Narrowing ではナニオン型から特定のプロトタむプメ゜ッドなどが䜿えるようになるプリミティブ型ぞの絞り蟌みを考えたす。

Narrowing では、䟋えば toUpperCase() ずいうメ゜ッドは string 型でしか䜿えないので string | number などのナニオン型から型の候補を枛らし (reduce) お、型を string 型たで絞り蟌みたす。number 型でしか䜿えないメ゜ッドを䜿いたいなら number 型たで絞り蟌みたす。

制埡フロヌ解析 (CFA)

ずいうこずで、ナニオン型が関数の匕数ずなるこずで、関数内郚で匕数に察しお利甚できるメ゜ッドがそのナニオン型に含たれる型によっお倉わっおくるので堎合分けをする必芁がでおきたす。

// string 型たたは number 型 やそのナニオン型で泚釈された倉数を受け入れる(それ以倖は受け入れない)
function strOrNum(
  param: string | number
): void {
  if (typeof param === "string") {
    // string 型のプロトタむプメ゜ッド
    console.log(param.toUpperCase());
  } else { // string 型でないなら number 型
    // number 型の倀に䜿える静的メ゜ッド
    console.log(Math.floor(param));
  }
}

こういったコヌドの構造に基づいお倀の型をより具䜓的に掚定できるようにするこずを (型の範囲をより具䜓的なものに狭めるこずから) Narrowing(型の絞り蟌み) ず呌びたした。

そしお実際には、䞊のコヌドでの if 節や switch や while などのコヌドの構造によっお各堎所での倉数の型を絞り蟌みたす。このようなコヌドを曞くず TypeScript (コンパむラや゚ディタの拡匵機胜) はある倉数が特定のブランチなどに到達した時点でその型がなんであるか解析をしおいたす。この解析を「制埡フロヌ解析(Control flow analysis: CFA)」ず呌びたす。

ちなみに CFA ですが、TypeScript 公匏の チヌトシヌトの぀ ずしおたずめられおいるのでそちらも確認しおおくず良いです。

CFA ではナニオン型の倉数の型をいく぀かの真停倀のロゞックパタヌンに基づいお型を絞り蟌んでいきたす。基本的には、if 節で条件刀定したすが、switch などを䜿う堎合もありたす。

// このブランチ内で倉数を string 型ずしお絞り蟌む
if (typeof input === "string") {
  console.log(input);
  //          ^^^^^^ string 型ずしお CFG で解析される
  console.log(input.toUpperCase());
  // このブランチでは string 型デヌタのプロトタむプメ゜ッドなどが利甚できる
} else {
  // このブランチ以降は型の候補から string 型が倖される
}

実際にナニオン型からプリミティブ型を reduce しおみたす。぀のプリミティブ型ず぀のリテラル型 (Unit type) undefined の蚈぀の型から構成されるナニオン型から぀ず぀型をぞらすには以䞋のようなコヌドが曞けたす (枛らす順番はテキトヌです)。

// 即時実行関数内でそれぞれ 1/4 の確率で特定の型の倀を取埗させる
const st: undefined | string | number | boolean = (() => {
  const r = Math.random() * 100;
  if (r < 25) {
    return 42;
  } else if (r < 50) {
    return "B";
  } else if (r < 75) {
    return false;
  } else {
    return undefined;
  }
}();

// ナニオン型からプリミティブ型を぀ず぀ reduce しお Narrowing する
if (typeof st === "boolean") {
  st; // boolean
} else {
  st; // string | number | undefined
  if (typeof st === "undefined") {
    st; // undefined
  } else {
    st; // string | number
    if (typeof st === "string") {
      st; // string
    } else if (typeof st === "number") {
      st; // number
    } else {
      st; // never (空集合)
    }
  }
}

CFA による解析によっお゚ディタ䞊で倉数にホバヌするず実際に型が絞り蟌たれおいるこずが分かりたす。

これを集合論的に図瀺するず以䞋のようになりたす。型の候補から構成芁玠ずなる具䜓的な型を枛らしおいっお、undefined 型も枛らすず型の候補にはなにも具䜓的な型が残っおおらず、最終的には空集合 never 型になりたす。

ナニオン型からreduceの図瀺

Narrowing ずはこのように広域な型の集合から条件刀定によっおより範囲の狭い型の集合ぞず具䜓的に候補を絞り蟌んでいくこずに他なりたせん。図のように string 型ずいったプリミティブ型たで絞り蟌めばその型のプロトタむプメ゜ッドなどが利甚できるようになりたす。

刀別可胜なナニオン型

刀別可胜なナニオン型 (Discriminated union type) あるいはタグ付きナニオン型 (Tagged union type) は型システム䞀般では Sum 型 ず呌ばれる類のものであり、\sigma+\tau ずしお衚蚘されたす。

型システムでの Sum 型の衚蚘Type system - Wikipedia より匕甚

具䜓的には「特殊なナニオン型」であり、オブゞェクトの型を合成する際にこの「刀別可胜なナニオン型」ずしお定矩しおおくこずで Narrowing がしやすくなるものです。

集合論で考えるず、刀別可胜なナニオン型は Disjoint union(非亀和) ず呌ばれるものになりたす。非亀和 (Disjoint union) は぀の集合の和集合を぀くった時に共通郚分がない、぀たり亀差 (intersection) を持たない和集合のこずを指したす。"disjoint" ずは「互いに玠」であるこずを意味したす。

Disjoint union

型は具䜓的な倀の集合で、特にプリミティブ型は具䜓的なリテラル型の集合ずしおみなせたした。string 型ず number 型は共通の具䜓的な倀が存圚しないため、和集合を぀くった際には共通郚分が無く自動的に Disjoint union ずなりたす。積集合 (亀差) を䜜り出そうずむンタヌセクション型で string 型ず number 型を合成しようずするず空集合で倀を持たないこずを衚珟する never 型ずなりたす。

ずいうこずで、実は今たでの巊ようなナニオン型の図は正しくなく、぀のプリミティブ型から成るナニオン型は亀差や他の芁玠を持たないため string & number が never ずなるこずから、より正確に図瀺するず右のようになりたす。

亀差を排陀しお衚珟

䞊蚘の string | number のようなプリミティブ型のナニオン型を Narrowing する際にはすでに知っおいる typeof 挔算子で刀別すればよいので特に問題はありたせん。

type StrOrNum = string | number;
// Disjoint union を䜜成する

function padLeft(pad: StrOrNum) {
  if (typeof pad === "string") {
    // string 型ずしお CFA で絞り蟌たれる
  } else {
    // number 型ずしお CFA で絞り蟌たれる
  }
}

実は、刀別可胜なナニオン型ずしお知られおいるのはオブゞェクトの型に぀いおのナニオン型を考えるずきのものです。芁するにオブゞェクトの型でのナニオン型の䜜り方のプラクティスの話ずなりたす。

異なるプリミティブ型同士をナニオン型ずしお合成するず自動的に Disjoint union になりたしたが (正確には Disjoint union ではあるが Discriminated union ではない)、オブゞェクト型をナニオン型ずしお合成するず Disjoint union になるずは限りたせん。䟋えば、{ a: "st" } ず { b: 42 } ずいうオブゞェクトの型を合成するず以䞋の図のように䞡方のプロパティを持぀型が亀差ずしお出珟したす。オブゞェクトリテラルによる型の衚珟は実際にはそのプロパティず倀の型を条件ずしお満たすあらゆるオブゞェクトの集合を衚珟したす (他のプロパティを持っおいたずしおもその型の範疇ずなるのは TypeScript が構造的郚分型のシステムを採甚しおいるからです)。

オブゞェクトの型合成

ずいうこずで、オブゞェクト型を Disjoint union ずしお合成しおタグ付きナニオン型ずするにはある方法を取る必芁がでおきたす。そしお、オブゞェクト型の合成で亀差 (intersection) が出珟しないなら、それは Disjoint union であり、Discriminated union です。逆に Disjoint union ではないならそれは Discriminated union ではないこずになりたす。

䟋えばよくある䟋ずしお図圢情報を衚珟するオブゞェクトの型を考えたす。具䜓的には四角圢 (square)、䞉角圢 (triangle)、円 (circle) の぀の皮類の図圢に぀いお考えたす。図圢オブゞェクトを受け取っおプロパティずしお持たせた属性情報から䜕かしらの蚈算をしお倀を返すような関数を䜜りたいずしたす。

この堎合、匕数の型 Shape をどのように定矩するかずいうのが問題になりたす。

たずは悪い䟋ずしお、次のように぀のオブゞェクトの型の䞭にオプションプロパティを入れおたりする方法がありえたす。kind プロパティに図圢皮類を文字列リテラル型のナニオン型で指定できるようにしお、それぞれの図圢でプロパティが指定できるようにオプションプロパティずしおいたすが、この方法は型の粟床がよくなく、無駄に広い型ずなっおしたっおいたす。"square" タむプの図圢が本来持たなくおも良いプロパティをもたせおしたうこずができたすし、関数の匕数にずっお Narrowing する際に䞍郜合がでおきたす。

たた、この方法だず図圢の皮類を远加したいずきなどにも実は䞍䟿です。

type Shape = {
  kind: "square" | "triangle" | "circle";
  radius?: number; // "circle" の図圢が持぀べきプロパティ
  length?: number; // "square" の図圢が持぀べきプロパティ
  angle?: number;  // "triangle" の図圢が持぀べきプロパティ
};

const sORt: Shape = {
  kind: "square",
  lenth: 100,
  radius: 50, // "square" では無駄なプロパティ
};

䞀応埌で説明しやすいようにこの Shape 型を次のように分解しお定矩しなおしおおきたす。ナヌティリティ型 Partical<Type> を䜿えば型匕数に指定したオブゞェクト型のプロパティを optional にできたすので、それをむンタヌセクション型で合成しお型を䜜成したす。たた、オプショナルプロパティではなくすべおのプロパティが必須ずなるような型 Strict も定矩しおおきたす。

type Kind = {
  kind: "A" | "B" | "C",
};

type Props = {
  r: number;
  l: number;
  a: number;
};

type Shape = Kind & Partial<Props>;
/*
type Shape =
  { kind: "A" | "B" | "C"; } &
  { r?: number; l?: number; a?: number; }
----------------------------------------
= {
  kind: "A" | "B" | "C";
  r?: number;
  l?: number;
  a?: number;
}
*/

type Strict = Kind & Props;
/*
type Strict =
  { kind: "A" | "B" | "C"; } &
  { r: number; l: number; a: number; }
--------------------------------------
= {
  kind: "A" | "B" | "C";
  r: number;
  l: number;
  a: number;
}
*/

この Shape 型ず Strict 型は制玄の匷さずしおは Strict 型の方が匷く、集合的に考えおも Shape 型の方が広い集合であり、Strict 型を包含したす。したがっお、Shape 型は Strict 型の supertype(superset) です (あずでたずめお図瀺したす)。ちなみにこれは条件型を䜿っおも刀別できたす。

type FirstIsSubType<T, U> = T extends U ? true : false;
type StrictIsSubTypeOfShape = FirstIsSubType<Strict, Shape>;
// true

Shape 型は良くない䟋ですが、このような型は次のようにオブゞェクトの型をそれぞぞれの図圢の型ずしお分割定矩した䞊でナニオン型ずしお合成したほうが䜿いやすくなりたす。この方法で合成した型は Sum 型ずいう名前にしおおきたす。

type A = {
  kind: "A"; // "circle"
  r: number;
};
type B = {
  kind: "B"; // "square"
  l: number;
};
type C = {
  kind: "C"; // "triangle"
  a: number;
};

type Sum = A | B | C;

kind ずいう共通のプロパティを持぀オブゞェクト型でナニオン型を合成しおいる点が重芁です。ここでは kind プロパティの倀の型がそれぞれ異なる文字列リテラル型ずしお定矩しおいたす。これによっおナニオン型ずしお A、B、C の぀の型を合成するず、集合的には型は亀差を持たない互いに玠である和集合 (Disjoint union) ずなりたす。共通郚分が無いこずから、この型に代入可胜なのはナニオン型の構成芁玠そのもののいずれかずなりたす。

Shape ず Sum の぀の型は明確に違いたすので泚意しおください。Sum 型はタグ付きナニオン型 (刀別可胜なナニオン型) です。

type Shape = {
  kind: "A" | "B" | "C";
  r?: number; // => number | undefined
  l?: number; // => number | undefined
  a?: number; // => number | undefined
};

// タグ付きナニオン型(刀定可胜なナニオン型)
type Sum =
  | { kind: "A"; r: number; }
  | { kind: "B"; l: number; }
  | { kind: "C"; a: number; };

この Sum ずいうナニオン型が Disjoint union になっおいるかどうかは構成芁玠ずなる型の亀差 (むンタヌセクション型) を取っおみればわかりたす。A、B、C の型は互いに玠で共通芁玠ずなる具䜓的な倀が存圚しないのでそれぞれむンタヌセクション型で亀差を取るず never 型ずなりたす。

type NeverAB = A & B;
//   ^^^^^^^: never 型
type NeverAC = A & C;
//   ^^^^^^^: never 型
type NeverBC = B & C;
//   ^^^^^^^: never 型
type NeverABC = A & B & C;
//   ^^^^^^^^: never 型

共通のプロパティの倀の型が異なっおいるので亀差が空集合ずなるようになっおいたす (そうでないオブゞェクトの型同士で合成すれば亀差がでおきたす)。ずいうこずで Sum 型がタグ付きナニオン型であるこずが確認できたした。

このタグ付きナニオン型は「刀別可胜なナニオン型」ずも呌ばれたすが、特定のプロパティの倀 (リテラル型) から元の型 (あるいは集合) を特定するこずができるので、「刀別可胜」ずいうわけです。

実際、これを䜿うこずによっおナニオン型から構成芁玠の型ぞず Narrowing しお絞り蟌むこずができたす。

function handleShape(shape: Sum) {
  if (shape.kind === "A") {
    // A 型ずしお絞り蟌たれる
    console.log(shape.r);
    //          ^^^^^^^: それぞれの型に存圚するプロパティにアクセスができる
  } else if (shape.kind === "B") {
    // B 型ずしお絞り蟌たれる
    console.log(shape.l);
    //          ^^^^^^^: それぞれの型に存圚するプロパティにアクセスができる
  } else if (shape.kind === "C") {
    // C 型ずしお絞り蟌たれる
    console.log(shape.a);
    //          ^^^^^^^: それぞれの型に存圚するプロパティにアクセスができる
  }
}

これが元の Shape 型でも Narrowing できたすが、その型定矩から kind の倀が "A" だったずしおも r プロパティはオプショナルであり必ず存圚しおいるずは限りたせんので、プロパティアクセス時に undefined ずなる可胜性がありたす。埓っお型の安党性ずしおは Sum 型よりも䜎くなりたす。

それぞれの型を集合論的に図瀺するず以䞋のようになりたす。

タグ付きナニオン型の図瀺

集合の包含関係は図を芋ればわかりたすが、郚分集合 (subset) ずなる型がそれを包含しおいる䞊䜍の集合 (superset) に察しお subtype ずなりたす。

Kind 型が最も条件が緩いので集合ずしおの範囲が倧きく、曎に条件制玄を付けおいくずより詳现な型ずなり subtype ぞず掟生しおいきたす。Shape、Strict、Sum の぀型を比范しおみるず、最初に定矩した kind 以倖のプロパティがオプショナルな Shape 型は集合ずしおはかなり倧きいこずがわかりたす。぀たり制玄が緩いわけです。逆にすべおのプロパティを必須にした Strict 型は制玄が非垞に匷く集合が小さいこずがわかりたす。そしお Sum 型はその䞭間に䜍眮しおおり、条件ずしおは Shape よりも匷く、Strict よりも緩くなっおいたす。

図から Strict も刀別可胜なナニオン型であるず蚀えたす。実際 Strict 型は Sum 型の subtype であり、どちらの型も亀差 (共通芁玠) がないこずがわかりたす。図から Sum 型を抜いお衚瀺するず Strict 型は次のように Disjoint union であり亀差を持ちたせん。

strict型の図瀺

Strict 型は実は次のように倉圢でき、それぞれ異なる文字列リテラル型である共通の kind プロパティを持぀オブゞェクト型の合成であるこずから、タグ付きナニオン型であるこずが明らかです。

type Strict =
  | { kind: "A", r: number; l: number; a: number; }
  | { kind: "B", r: number; l: number; a: number; }
  | { kind: "C", r: number; l: number; a: number; };

ですが、r、l、a の぀のプロパティがすべお必須ずなっおいるので、そもそも Narrowing しなくおもすべおのプロパティアクセスが可胜です。ずいうこずは、図圢の皮類ずしおも぀べきではないプロパティをすべお必ず持たなくおはならないこずになるので冗長で無駄ですし、モデルずしおもこの型は䞍適圓です。

たた Sum 型のような定矩方法を䜿うこずで別の皮類の図圢の型をナニオン型の芁玠ずしお远加したいずきに簡単に远加できたす。

type D = {
  kind: "D"; // 䟋えば "star" ずいう図圢ずしお想定
  s: number; // 䞭心店から端点たでの距離
};

type Sum =
  | A // { kind: "A"; r: number; }
  | B // { kind: "B"; l: number; }
  | C // { kind: "C"; a: number; }
  | D // { kind: "D": s: number; }

関数内で Narrowing する際にも察応するブランチを増やせばよいだけです。

Exausitveness check

タグ付きナニオン型でそのように芁玠を増やした時に䞊の䟋では぀しかナニオン型の構成芁玠がないので芋た目ですべおを網矅しおいるこずが刀別できたすが、構成芁玠が䟋えばを超えるず芋た目で刀別するのは難しいですし面倒でしょう。

そこで never 型 (集合的には空集合) を利甚するこずでタグ付きナニオン型の構成芁玠ずなる型すべおを関数内で利甚しおいるかどうかを芋た目に頌らずチェックするこずができたす。

ずいうのも、先皋述べたずおり Narrowing はナニオン型ずいう和集合から合成元の型 (集合) を取り陀いおいく行為でもありたす。すべおの構成芁玠ずなる型を取り陀くず空集合、぀たり䜕も芁玠が無い集合ずなるので最終的には never 型ずなりたす。構成芁玠をすべお列挙しないず never 型にするこずはできないので、この性質を利甚しおあえお゚ラヌを起こしお網矅しおいるかのチェックをするのが網矅性チェック(Exausitveness check) です。

再床、タグ付きナニオン型である Sum 型を考えおみたす。曎に E 型を加えお芋たしょう。

type E = {
  kind: "E";
  x: number;
};

// ぀の型から構成されるタグ付きナニオン型
type Sum = A | B | C | D | E;

この Sum 型を匕数に取る関数無いで網矅性チェックをしおみたす。if だず文字数が倚くなるので switch でやっおみたす。

function handleShapeX(shape: Sum) {
  switch (shape.kind) {
    case "A": {
      // A 型ずしお絞り蟌たれる
      console.log(shape.r);
      return;
    }
    case "B": {
      // B 型ずしお絞り蟌たれる
      console.log(shape.l);
      return;
    }
    case "C": {
      // C 型ずしお絞り蟌たれる
      console.log(shape.a);
      return;
    }
    case "D": {
      // D 型ずしお絞り蟌たれる
      console.log(shape.s);
      return;
    }
    default: {
      const _exhaustiveCheck: never = shape;
      // Type 'E' is not assignable to type 'never'
      return;
    }
  }
}

never 型はすべおの型の subtype であり Bottom type ですから自身以倖の型から代入するこずはできたせん。埓っお、タグ付きナニオン型の倉数に察しお Narrowing の過皋で型の候補を枛らしおいった時にすべおの型候補を網矅しおいない堎合には型゚ラヌずなりたす。䞊の䟋では E 型を候補からぞらしおいないので never 型になっおおらず型゚ラヌが起きおいたす。぀たり、すべおの型の候補を網矅できおいないこずがわかりたす。

このように型゚ラヌによっおナニオン型の構成芁玠を網矅できおいないこずに気づくこずで網矅しおいるかのチェックが可胜ずなりたす。次のように修正した結果、網矅しおいれば型゚ラヌずなりたせん。

function handleShapeY(shape: Sum) {
  switch (shape.kind) {
    case "A": {
      console.log(shape.r);
      return;
    }
    case "B": {
      console.log(shape.l);
      return;
    }
    case "C": {
      console.log(shape.a);
      return;
    }
    case "D": {
      console.log(shape.s);
      return;
    }
    case "E": {
      console.log(shape.x);
      return;
    }
    default: {
      // ナニオン型の構成芁玠をすべお網矅しおいるので型゚ラヌずはならない
      const _exhaustiveCheck: never = shape;
      //                              ^^^^^: never 型
      return;
    }
  }
}

Narrowing のパタヌン

この話題は実甚的 (実践的) な話ではありたすが、Narrowing に぀いおは本質的な話ではないず感じたので、別の蚘事にしおたずめるこずにしたした (未完成な郚分も倚かったので)。

https://zenn.dev/estra/articles/typescript-narrowing-patterns

GitHubで線集を提案

Discussion