
TypeScriptのナローイングについて公式ドキュメントを読んでみる
TypeScriptでは、
タイプガードと呼ばれる特別なチェックと代入に注目し、
宣言された型よりもさらに具体的な型に絞り込むナローイングというプロセスを作ることが出来ます。
今日の記事では、このナローイングについて、公式ドキュメントを参考にしながら、
分かりやすく解説していきます。
まず、padLeft関数があることをイメージしてみてください。
function padLeft(padding: number | string, input: string): string {
throw new Error("まだ開発されてない!")
}
この関数をこういう風に実装します。
function padLeft(padding: number | string, input: string) {
return new Array(padding + 1).join(" ") + input;
}
すると、こういうエラーが出ますね
Operator '+' cannot be applied to types 'string | number' and 'number'.
padding + 1でエラーが発生します。
TypeScriptは、数値|文字列に数値を追加しても、
欲しいものが得られないかもしれないと警告していますが、それは正しいです。
言い換えれば、paddingが数字であるかどうかを最初に明示的にチェックしていないし、
文字列である場合の処理もしていないので、正確にチェックしないといけません。
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
}
return padding + input;
}
ある意味このコードはJavaScriptのコードのように見えるかもしれません
事実、配置したアノテーションを除けば、このTypeScriptコードはJavaScriptのように見えます。
でもTypeScriptの型システムは、型安全性を確保するために無理をしなくても、
典型的なJavaScriptのコードをできるだけ簡単に書けるようにすることを目的としています。
また、TypeScriptは、プログラムが取りうる実行経路をたどって、与えられた位置にある値の可能な限り具体的な型を分析します。
タイプガードと呼ばれる特別なチェックと代入に注目し、宣言された型よりもさらに具体的な型に絞り込むプロセスをナローイングと呼びます。
多くのエディタでは、これらの型が変化していく様子を観察することができますし、例題でもそうしていきます。
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
// ^ = (parameter) padding: number
}
return padding + input;
// ^ = (parameter) padding: string
}
TypeScriptが絞り込みのために理解する構造にはいくつかの種類があります。
typeof
type guards
JavaScriptはtypeof演算子をサポートしており、
実行時に持つ値の型について非常に基本的な情報を与えることができます。
TypeScriptでは、この演算子が特定の文字列のセットを返すことを期待しています。
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"
padLeftで見たように、この演算子は多くのJavaScriptライブラリで頻繁に登場しています。
そしてTypeScriptでは異なる分岐で型を絞り込むために理解することができます。
TypeScriptでは、typeofが返す値をチェックすることをタイプガードと呼びます。
TypeScriptはtypeofがさまざまな値に対してどのように動作するかをコード化しているため、
JavaScriptにおけるtypeofのいくつかのクセを知っています。
たとえば、上のリストでは、typeofは文字列nullを返さないことに注目してください。
次の例を見てください。
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) {
// Object is possibly 'null'.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
printAll関数では、strsがオブジェクトであるかどうかをチェックして、
配列型であるかどうかを確認しようとしています。
しかし、JavaScriptではtypeof nullは実際には “object “になってしまします。
これは歴史上の不幸な事故の1つです。
幸運なことに、TypeScriptではstrsが単なるstring[] ではなくstring[] | nullに絞られていたことを知ることができます。
これは私たちが「真実性」チェックと呼ぶものへの良いきっかけになるかもしれません。
真実性の狭間
真実性は、辞書には載っていない言葉かもしれませんが、
JavaScriptでは非常によく耳にする言葉です。
JavaScriptでは、条件式、&&s、||s、if文、ブールの否定(!)など、
どんな表現でも使うことができます。
function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `There are ${numUsersOnline} online now!`;
}
return "Nobody's here. :(";
}
JavaScriptでは、ifのような構造体は、
まず条件を意味のあるものにするためにブーリアンに「coerce」し、
その結果が真か偽かによって分岐を選択します。
- 0
- NaN
- “” (空の文字列)
- 0n (ゼロのビッグイント版)
- null
- 未定義
これらはすべて偽に変換され、その他の値は真に変換されます。
値をブール関数に通すか、より短い二重ブール否定を使うことで、
常にブール値に補整することができます。
特にnullやundefinedのような値を防ぐために、この動作を利用することがよく行われています。例として、printAll関数に使ってみましょう。
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
このようにstrsがtruthyであるかどうかをチェックすることで
少なくとも、nullのような恐ろしいエラーを防ぐことができます。
TypeError: null is not iterable
しかし、プリミティブの真実性のチェックは、しばしばエラーになりやすいことを覚えておいてください。
例として、printAllを書く際の別の試みを考えてみましょう。
function printAll(strs: string | string[] | null) {
// !!!!!!!!!!!!!!!!
// DON'T DO THIS!
// KEEP READING
// !!!!!!!!!!!!!!!!
if (strs) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}
しかし、これには微妙な欠点があります。空文字列のケースを正しく処理できなくなってしまうのです。
TypeScriptはここでは全く問題になりませんが、これはJavaScriptにあまり慣れていない人には注意する価値のある動作です。
TypeScriptはバグを早期に発見するのに役立つことが多いのですが、値に何もしないことを選択した場合、
過度に規定することなくできることは限られています。
必要であれば、このような状況をリンターで処理するようにすることもできます。
真実性による絞り込みについて最後に一言、ブールの否定に ! をつけると、
否定された枝からフィルタリングされます。
function multiplyAll(
values: number[] | undefined,
factor: number
): number[] | undefined {
if (!values) {
return values;
} else {
return values.map((x) => x * factor);
}
}
等式の絞り込み
TypeScriptでは、switch文や、===、!==、==、!==のような等値性チェックを使って、
型を絞り込むこともできます。たとえば、以下のようになります。
function example(x: string | number, y: string | boolean) {
if (x === y) {
// We can now call any 'string' method on 'x' or 'y'.
x.toUpperCase();
// ^ = (method) String.toUpperCase(): string
y.toLowerCase();
// ^ = (method) String.toLowerCase(): string
} else {
console.log(x);
// ^ = (parameter) x: string | number
console.log(y);
// ^ = (parameter) y: string | boolean
}
}
上の例では、最初のif文で、xとyの両方が取り得る共通の型はstringだけだと分かるので、
TypeScriptは最初の分岐でxとyが文字列であると型を絞り込むことができます。
また、特定のリテラル値(変数ではなく)に対するチェックも有効です。
function printAll(strs: string | string[] | null) {
if (strs !== null) {
if (typeof strs === "object") {
for (const s of strs) {
// ^ = (parameter) strs: string[]
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
// ^ = (parameter) strs: string
}
}
}
また、== nullをチェックするということは、
実際にそれが具体的にnullという値であるかどうかをチェックするだけでなく、
潜在的に未定義であるかどうかもチェックするということです。
同じことが == undefined にも当てはまり、
ある値が null か undefined のどちらかであるかをチェックすることが出来ます
インターフェース コンテナ {
value: 数字|null|undefined;
}
function multiplyValue(container: Container, factor: number) { 。
// 型から'null'と'undefined'の両方を取り除きます。
if (container.value != null) {
console.log(container.value);
// ^ = (プロパティ) Container.value: number
// これで、'container.value'を安全に乗算できるようになりました。
container.value *= factor;
}
}
とても便利ですね。
instanceof ナローイング
JavaScriptには、ある値が他の値の「インスタンス」であるかどうかをチェックする演算子があります。
具体的には、JavaScriptのx instanceof Fooは、xのプロトタイプチェーンにFoo.prototypeが含まれているかどうかをチェックできます。
newを使って構築できるほとんどの値には便利なもで、instanceofはタイプガードでもあり、
TypeScriptではinstanceofでガードされた分岐を狭めることができます。
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
// ^ = (parameter) x: Date
} else {
console.log(x.toUpperCase());
// ^ = (parameter) x: string
}
}
代入について
TypeScriptは代入の右辺を見て、左辺を適切に絞り込みます。
let x = Math.random() < 0.5 ? 10 : "hello world!";
// ^ = let x: string | number
x = 1;
console.log(x);
// ^ = let x: number
x = "goodbye!";
console.log(x);
// ^ = let x: string
ちなみにですが、興味深いのは、
xの宣言された型(xが最初にあった型)がstring | numberである場合、
代入可能性な値は常に宣言された型に対してチェックされます。
もしxにbooleanを代入していたら、宣言された型に含まれていないのでエラーになります。
let x = Math.random() < 0.5 ? 10 : "hello world!";
// ^ = let x: string | number
x = 1;
console.log(x);
// ^ = let x: number
x = true;
Type 'boolean' is not assignable to type 'string | number'.
console.log(x);
// ^ = let x: string | number
コントロールフロー解析
ここまでは、TypeScriptが特定の分岐の中でどのように絞り込むかについて、
いくつかの基本的な例を見てきましたが、
if、whiles、conditional以外の、もう少し多くのことも出来まして、
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
}
return padding + input;
}
padLeftは最初のifブロックの中でリターンしてますが
TypeScriptはこのコードを分析して、paddingが数字の場合には、
本体の残りの部分(return padding + input;)に到達できないことを確認できます。
その結果、関数の残りの部分では、文字列から数字への絞り込みが出来ます。
このように到達可能性に基づいてコードを解析することを制御フロー解析といい、
TypeScriptではタイプガードや代入に遭遇した際に、このフロー解析を用いて型を絞り込んでいます。
変数が解析されると、制御フローが何度も分岐したり再合流したりして、
その変数が各ポイントで異なる型を持っていることが観察できます。
function example() {
let x: string | number | boolean;
x = Math.random() < 0.5;
console.log(x);
// ^ = let x: boolean
if (Math.random() < 0.5) {
x = "hello";
console.log(x);
// ^ = let x: string
} else {
x = 100;
console.log(x);
// ^ = let x: number
}
return x;
// ^ = let x: string | number
}
Using type predicates
ここまではJavaScriptの既存の構造を使って絞り込みを行ってきましたが、
コード全体で型がどのように変化するかをより直接的にコントロールしたい場合もあります。
ユーザー定義のタイプガードを定義するには、
単に戻り値の型がtype predicateである関数を定義する必要があります。
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
pet is Fishがこの例のtype predicateです。
type predicateは parameterName is Type という形式で、parameterName には現在の関数シグネチャのパラメータ名を指定します。
isFishが何らかの変数とともに呼び出されると、TypeScriptは元の型に互換性があれば、その変数を特定の型に絞り込みます。
// 'swim'と'fly'の両方の呼び出でます
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
TypeScriptはif分岐でpetがFishであることを知っているだけでなく、
else分岐ではFishがないからBirdでなければならないことも知っていますよね。
タイプガード isFish を使って Fish | Bird の配列をフィルタリングし、
Fish の配列を得ることができます。
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// or, equivalently
const underWater2: Fish[] = zoo.filter(isFish) as Fish[];
// The predicate may need repeating for more complex examples
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
if (pet.name === "sharkey") return false;
return isFish(pet);
});