Home About Contact
TypeScript , Functional Programming

type の種類ごとにガードしたい

Apple 製品と MS 製品の混在したリストがあったとして、そこから Apple 製品だけ、または MS 製品だけのリストをつくるという例を考える。

そのための事前準備として次の type を定義をする:

type Apple = 'macbook pro' | 'macbook air' | 'mac mini' | 'mac studio'
type MS    = 'surface pro' | 'surface go' | 'surface laptop'

そして Apple または MS の両方を表現した type を定義:

type Machine = Apple | MS

さらに今回実験で使う Apple と MS 製品の両方を含んだリスト xs :

const xs: Machine[] = ['surface go', 'mac mini', 'surface laptop']

なお実験では deno を使って typescript を実行します。

$ deno --version
deno 2.4.1 (stable, release, x86_64-unknown-linux-gnu)
v8 13.7.152.6-rusty
typescript 5.8.3

与えられた Machine が Apple か MS かを判別するガード用関数を定義:

const isApple = (v: Machine): boolean => { return v.startsWith('mac') }
const isMS    = (v: Machine): boolean => { return v.startsWith('surface') }

これを使えば、リスト xs から Apple 製品だけを取り出したリストをつくるには次のようになります。

const appleMachines = xs.filter((it: Machine)=> isApple(it)) 

ここまでをコードにして実行してみます。

コード:

// main.ts

type Apple = 'macbook pro' | 'macbook air' | 'mac mini' | 'mac studio'
type MS    = 'surface pro' | 'surface go' | 'surface laptop'

type Machine = Apple | MS

const isApple = (v: Machine): boolean => { return v.startsWith('mac') }
const isMS    = (v: Machine): boolean => { return v.startsWith('surface') }

const xs: Machine[] = ['surface go', 'mac mini', 'surface laptop']

const appleMachines = xs.filter((it: Machine)=> isApple(it))
console.log(appleMachines)

実行:

$ deno --check main.ts 
[ "mac mini" ]

これでガードできました。

ただ・・・ここからが本題なのですが、たとえば MyMachines という自分が持っているマシンを表現する type を定義するとします:

type MyMachines = {
    appleMachines: Apple[]
    msMachines: MS[]
}

そして、ここに先ほど計算した appleMachines を入れる:

const myMachines: MyMachines = {
    appleMachines: appleMachines,
    msMachines: []
}

実行する:

$ deno --check main.ts
TS2322 [ERROR]: Type 'Machine[]' is not assignable to type 'Apple[]'.
  Type 'Machine' is not assignable to type 'Apple'.
    Type '"surface pro"' is not assignable to type 'Apple'.
        appleMachines: appleMachines,
        ~~~~~~~~~~~~~

    The expected type comes from property 'appleMachines' which is declared here on type 'MyMachines'
        appleMachines: Apple[]
        ~~~~~~~~~~~~~

error: Type checking failed.

type check で エラーになります。

そもそも、この部分:

const appleMachines = xs.filter((it: Machine)=> isApple(it))

ここで appleMachines: Apple[] の型に(こちらの脳内では)なっているが、実際は appleMachines: Machine[] にしかなっていない。 つまり コンパイラからみれば appleMachines の各要素の type は 依然として Apple | MS (つまり Machine ってことですけど)のまま。

これどうしたら typeApple に限定できるか?と思って調べたのですが(Claude にきいただけです)、 結論としてはガード関数の戻値を boolean ではなく v is Apple に変更すればOKでした。

つまり:

// const isApple = (v: Machine): boolean => { return v.startsWith('mac') }
const isApple = (v: Machine): v is Apple => { return v.startsWith('mac') }

v is Apple という型があるの?(よくわかっていない)まあこれで意図通り作動します。

MS の方も同じように変更。 そうやって、完成したコードはこちら:

// main.ts

type Apple = 'macbook pro' | 'macbook air' | 'mac mini' | 'mac studio'
type MS    = 'surface pro' | 'surface go' | 'surface laptop'

type Machine = Apple | MS

const isApple = (v: Machine): v is Apple => { return v.startsWith('mac') }
const isMS    = (v: Machine): v is MS => { return v.startsWith('surface') }

const xs: Machine[] = ['surface go', 'mac mini', 'surface laptop']

const appleMachines: Apple[] = xs.filter((it: Machine)=> isApple(it))
const msMachines: MS[]       = xs.filter((it: Machine)=> isMS(it))

type MyMachines = {
    appleMachines: Apple[]
    msMachines: MS[]
}

const myMachines: MyMachines = {
    appleMachines: appleMachines,
    msMachines: msMachines
}

console.log(myMachines)

実行:

$ deno --check main.ts
{
  appleMachines: [ "mac mini" ],
  msMachines: [ "surface go", "surface laptop" ]
}

boolean ではなく v is Apple とか v is MS と記述して type を限定せよ、という話でした。

そのうち AIが棲んでいるコンパイラが登場して、AI提案を受けいれてコンパイルするオプションを指定した場合、 この手のコード(今回の例では boolean は v is Apple とか v is MS にするべき)は、 コンパイラ(の中のAI)が「こういう解釈でやっときますね」と言ってコードを修正した状態でコンパイルしてくれる機能とかありかも。
否、その手のはコンパイラではなくて Lint 系のツールがやることかもしれない。

そもそも VS Code のような IDE を使っていれば、IDE 上で AI がサジェストすればいいだけだから、コンパイラがそんな機能を持つ必要もないといえばないのですが。