🙂‍↕️

TypeScript 5.5で型述語を推論できて最高。配列のfilterも型安全に

2024/03/21に公開
3

2024/04/09
プロパティによる絞り込みが可能になったので追記しました。


TypeScriptの次バージョン5.5で、開発者が長い間求めていた挙動が手に入ります。

現状のTypeScript (執筆時点で5.4)では、ユーザー定義型ガードを使う際には型述語(用語は後ほど解説します)の記述が必要です。

function isNumber(value: number | string): value is number {
  return typeof value === 'number';
}

6月リリース予定のTypeScript 5.5では、関数の実体から型述語の型推論(infer type predicates)が可能になります。すなわち、次のようなコードが可能になります。

function isNumber(value: number | string) {
  return typeof value === 'number';
}

配列のfilterメソッドで型を絞り込む際にも、型述語の型推論が可能となります。たとえば、次のようなコードのようにx is Sの記述をせずともfilterメソッドで型が正しく推論されます。

const result = [12, null, 24, undefined, 48]
  .filter((value) => value != null);
//    resultはnumber[]に推論される

本記事では、 従来の型述語の危険性とTypeScript 5.5における型述語の型推論について具体的なコードを交えながら詳しく解説します。コードを動かせるプレイグラウンドのリンクも用意してあるので、ぜひ手を動かしてみて動作をご確認ください。

これまでの型述語の危険性

従来、TypeScriptには、ユーザー定義型カード(User-defined type guard)を使って型を絞り込むことができました。たとえば、次のような関数を定義することでvaluenumberであることをTypeScriptに伝えられます。 返り値のvalue is numberの箇所は型述語(type predicate)と呼ばれます。

function isNumber(value: number | string): value is number {
  return typeof value === 'number';
}

isNumber関数を使うと次のように値の絞り込みができます。

function isNumber(value: number | string): value is number {
  return typeof value === 'number';
}

function main(value: number | string) {
  if (isNumber(value)) {
    // valueは数値型に絞り込まれる
    value.toFixed(2);
  }
}

しかし、型述語には危険性があります。value is numberの箇所はユーザーが自身で定義しているものであり、関数本体の実装と一致しなくてもコンパイルエラーにならないのです。たとえば、次の例では関数本体ではtypeof value === 'string'valueを文字列判定していて、型述語としてはvalue is numberとなっているのですが、コンパイルエラーになりません。

function isNumber(value: number | string): value is number {
  // valueをnullだと判定しているが、エラーにならない
  return typeof value === null;
}

isNumber関数を使ったコードは型安全ではなく、ランタイムエラーを引き起こす可能性があります。

function isNumber(value: number | string): value is number {
  return typeof value === "string";
}

function main(value: number | string) {
  // 👎 ランタイムエラーになるまで気づけない
  if (isNumber(value)) {
    value.toFixed(2);
  }
}

main("豚骨きゅうり");

https://www.typescriptlang.org/play?ts=5.4.2#code/GYVwdgxgLglg9mABDAzgORAWwEYFMBOAFAG4CGANiLgFyJhZ76IA+iKU+MYA5gJS1lKuZCjoMCiAN4AoRIny4oIfEigBPAA644wRIKqIAvMcQAidpx6mA3NIC+06aEiwEiTKS4kKVWvRwSrBZcfFKyyLqEqBgBRPq4vLxhcnLxAHRQcABiMAAeuAAmhABMvLZyDg7SAPTViIC8G4ByO4iAlwyAzwyA-QyAJQyABwyAFQwtgD8MgNYMgFYMgNEMgH4MgOYMgOg2gKYMgIoMo4AiDNIeXqaAWjGAFVmAsgyAoQyAYgyAUQymZdJAA

TS Playgroundのエラー
TS PlaygroundでRunを実行したときにエラーが出ている様子

TypeScript 5.5から関数本体の実装から型を推論してくれるようになった

2024年6月リリース予定のTypeScript 5.5から型述語(x is S)の記述をすることなく、関数の本体から型述語が推論されるようになります。型述語の記述をしていない isNumber関数でも正しくタイプガードが行われています。

function isNumber(value: number | string) {
  return typeof value === 'number';
}

function main(value: number | string) {
  if (isNumber(value)) {
    // valueは数値型に絞り込まれる
    value.toFixed(2);
  }
}

https://www.typescriptlang.org/play?ts=5.5.0-dev.20240320#code/GYVwdgxgLglg9mABDAzgORAWwEYFMBOAFAG4CGANiLgFyLgDWYcA7mAJSIDeAUIovrigh8SKAE8ADrjjBEZSrkQBeFYgDkYLHnxqA3NwC+iXsdCRYCRJlIwwJClVoMmrDjz4xZhVBhwF7CmxuJnwA9KFyDriA9gyADqaAJAqA0eqA1gyAer6AUQyAPfGAfgyAMQyA0QwhkQoAdFBwAGIwAB64ACaEAExs+nxhEQB6APwmBoZAA

意図しない型の絞り込みを行っていた場合、コンパイルエラーとして気づけます。

function isNumber(value: number | string) {
  return typeof value === "string";
}

function main(value: number | string) {
  // 👍valueがstringに絞り込まれていることをコンパイルエラーとして気づける
  if (isNumber(value)) {
    value.toFixed(2);
  }
}

main("豚骨きゅうり");

https://www.typescriptlang.org/play?ts=5.5.0-dev.20240320#code/GYVwdgxgLglg9mABDAzgORAWwEYFMBOAFAG4CGANiLgFyJhZ76IA+iKU+MYA5gJSIBvAFCJE+XFBD4kUAJ4AHXHGCIylXIgC82xACJ2nHroDcQgL5ChoSLASJMpLiQpVa9HARZsOXPoJGIAPSBiIC8G4CyO2pUgDIMBr6A1gyAer6AUQyAPfGAfgyAMQyAZgyAIgyA0QyAygyAFgyASQyAzQyAzwyAiwyAJQyA1wyAFQyAlwyAPwzFgOoM2YDoNoCmDICKDPkBMCqEqBgeRFG4vPzCoqLTAHRQcABiMAAeuAAmhABMvKaiFhZCDk66gFoxgBVZgLIMgKEMgGIMybpHQA

型述語の推論結果の確認

TS PlaygroundやVSCodeでユーザー定義型ガードの関数を確認すると、返り値として型述語が推論されていることがわかります。

型述語の推論結果の確認

TypeScript 5.4以前では、型述語を記述しない場合の返り値はboolean型と推論されていました。

配列のfilterで型を絞り込むのがより型安全になる

本記法が便利なのは配列の filter メソッドで型を絞り込むときです。

filter メソッドでnullundefinedを取り除く処理というのは頻出します。 たとえば、数値と nullundefined が混在する配列からnullundefined を取り除いた配列を作るとします。次のようなコードが考えられるでしょう。

const result = [12, null, 24, undefined, 48]
                .filter((value) => value != null);

開発社は、resultの型はnumber[]に推論されることを期待するでしょう。

従来の課題

TypeScript 5.4以前では、resultnumber[]ではなく (number | null | undefined)[]にしか推論されませんでした。filter関数で明らかに nullundefined を除外しているにも関わらず、です。

https://www.typescriptlang.org/play?ts=5.4.2#code/MYewdgzgLgBATgUwgVwDawLwwNoEYBMANDGGqsfgCzHJgAmCAZgJZgJ3GUAcAugFAxBQ4SJgA6FugRwAFDIBuAQ1TIEAShgYAfDCUqEMAIRZSqVGoDcfIA

filter関数で絞り込まれる型を明示的に表現するため、型述語を使って次のように記述する方法が取られています。筆者もよく書くコードです。

const result = [12, null, 24, undefined, 48]
                .filter((value): value is number => value != null);

https://www.typescriptlang.org/play?ts=5.4.2#code/MYewdgzgLgBATgUwgVwDawLwwNoEYBMANDGGqsfgCzHJgAmCAZgJZgJ3GUAcAugFAxBQ4SJgA6FugRwAFDIBuAQ1TIEASgBcMJSoQxmEEsgC2AI2kwMAPm3LVMAIRZSqVGoDcQA

しかし、型述語はあくまでユーザー定義のものであり、誤った判定をしたとしてもコンパイルエラーになりません。

たとえば次の判定ではvaluenullのときにtrueを返してしまっていますが、value is number により resultnumber[] に推論されてしまいます。number用のメソッド toFixed()を使いresult[0].toFixed(2) と記述してしまうと、ランタイムエラーになるまで気づけません。

const result = [12, null, 24, undefined, 48]
                .filter((value): value is number => value === undefined);

// 👎コンパイルエラーにならず、ランタイムエラーになるまで気づけない
result[0].toFixed(2);

https://www.typescriptlang.org/play?ts=5.4.2#code/MYewdgzgLgBATgUwgVwDawLwwNoEYBMANDGGqsfgCzHJgAmCAZgJZgJ3GUAcAugFAxBQ4SJgA6FugRwAFDIBuAQ1TIEASgBcMJSoQxmEEsgC2AI2kwMAPm3LVljFloMWbOmoDcfPgHofMQF4NwDkdwGaGQGeGQEWGQBKGQGuGQAqGQEuGQB+GQGsGQCsGQEiGQC0GQEAGBLDAfoYowAOGRNS0wGiGQD8GQHMGQHQbQFMGQEUGNMARBj5EFHRsAAYeMSgQADFmAA92GXxPIA

TypeScript 5.5からの改善

TypeScript 5.5から、関数本体の実装から型述語を推論してくれるようになったのでx is S の記法が不要になります。

const result = [12, null, 24, undefined, 48]
                .filter((value) => value != null);

https://www.typescriptlang.org/play?ts=5.5.0-dev.20240320#code/MYewdgzgLgBATgUwgVwDawLwwNoEYBMANDGGqsfgCzHJgAmCAZgJZgJ3GUAcAugFAxBQ4SJgA6FugRwAFDIBuAQ1TIEAShgYAfDCUqEMAIRZSqVGoDcfPohTorAegcwAegH4gA

誤ってvaluenullのときにtrueを返すようなコードを書いた場合、result(number | null)[] に推論されます。number用のメソッド toFixed()を使い、result[0].toFixed(2) と記述したとき、ランタイムエラーではなくコンパイルエラーとして気づけます。

const result = [12, null, 24, undefined, 48]
                .filter((value) => value === undefined);

// コンパイルエラーになる👍
result[0].toFixed(2);

https://www.typescriptlang.org/play?ts=5.5.0-dev.20240320#code/MYewdgzgLgBATgUwgVwDawLwwNoEYBMANDGGqsfgCzHJgAmCAZgJZgJ3GUAcAugFAxBQ4SJgA6FugRwAFDIBuAQ1TIEAShgYAfDCUqEmjFloMWbOmoDcfPgHpbMQM0MgZ4ZAiwyAShkDXDIAqGQJcMgH4ZAawZAKwZAaIZAXg3AWR2+RBR0bAAGHjEoEAAxZgAPdhl8KyA

instanceofも使える

instanceofを使ったユーザー定義型ガードも、関数本体の実装から型述語が推論されます。

class Foo {}

class Bar {}

const result = [new Foo(), new Bar()].filter(x => x instanceof Foo);
//    ^?
//    Foo[] に推論される

https://www.typescriptlang.org/play?ts=5.5.0-dev.20240320#code/MYGwhgzhAEBiD29oG8C+AodpIwEJgCcUMt4A7CAF2gIFMIBXEagXmgG0zaB3ORACgCUAGmhde+AkIC6AOgBmAS2a0pAD2gsAfNA2KKlMGWC148vvEEBudAHpb0R9AB6AfjsOnCeO2nRA1gyAFcaAa1GAqgyAMQyA0QxAA

プロパティによる絞り込みも可能

※ 2024/04/09 追記

プロパティによる絞り込みを行うユーザー定義型ガードの結果も推論できます。具体的には、次のようなコードで isA関数の返り値が x is A と推論されることはないようです。本記事執筆当初(3/21)時点では推論不可能でしたが、現在ではできるようになっています。これがやりたかった・・・!

type A = { type: "A"; a: number };
type B = { type: "B"; b: number };

function isA(x: A | B) {
  return x.type === "A";
}

function check(foo: A | B) {
  if (isA(foo)) {
    // OK!
    console.log(foo.a);
  }
}

https://www.typescriptlang.org/play?ts=5.5.0-dev.20240408#code/C4TwDgpgBAglC8UDeVSQFxQEQywbigENMA7AVwFsAjCAJygF88AoNaAIQWVXAky3b4oVUpRr0mzZgDMyJAMbAAlgHsSUJQGcYACgAemOAB8o7AJTJmUKLQjAytdXoB0bBPEQ58zBlNkLlNSh5AAsIeQBrHWkVFUMoE3NLayVpKB0tXRiVMwskK2soAHoiqAB5AGlAQH+C63k1TRUAGwhnJpUAc2jY50IzFmtfXyA

filterの型の絞り込みが型安全になって最高

配列のfilterメソッドと型述語を使う度に、そのコードの危険性に震えていました。TypeScript 5.5での型述語の推論のおかげでその危険性がなくなるので一安心です。

https://twitter.com/tonkotsuboy_com/status/1769994147291889669

TypeScript 5.5は2024年6月18日のリリース予定です。楽しみに待ちましょう。

https://github.com/microsoft/TypeScript/issues/57475

参考記事

https://github.com/microsoft/TypeScript/pull/57465

https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates

https://www.totaltypescript.com/type-predicate-inference

GitHubで編集を提案
Ubie テックブログ

Discussion

さいぬさいぬ

プロパティでの型ガードが入ったのは熱いですね!これは正式リリースされたらすぐにアプデしたい

鹿野 壮鹿野 壮

激アツですよね〜😊
プロパティの型ガードの型述語推論、まあなくても便利だからいいかと思ったら、使えるようになって嬉しい。

たくみんたくみん

とてもいい記事をありがとうございます!

1点だけ、これまでの型述語の危険性のところで

次の例では関数本体ではtypeof value === 'string'とvalueを文字列判定していて

とあるのですが、コードを見てみるとnullと比較しているように見えます
どちらかに寄せるのが適切かなと思うのですが、いかがでしょうか?

こちらもし意図的なものであったり、自分の認識に間違いがあったら申し訳ありません!
よろしくお願いします!