Narrowing(型絞り込み)

padLeftという関数があるとします。

ts
function padLeft(padding: number | string, input: string): string {
throw new Error("Not implemented yet!");
}
Try

paddingnumber(数値)の場合、それをinputの前に追加するスペースの数として扱います。 paddingstring(文字列)の場合、単にpaddinginputの前に追加する必要があります。 padLeftpaddingとしてnumberが渡された場合のロジックを実装してみましょう。

ts
function padLeft(padding: number | string, input: string): string {
return " ".repeat(padding) + input;
Argument of type 'string | number' is not assignable to parameter of type 'number'. Type 'string' is not assignable to type 'number'.2345Argument of type 'string | number' is not assignable to parameter of type 'number'. Type 'string' is not assignable to type 'number'.
}
Try

おっと、paddingでエラーが発生しています。 TypeScriptは、number | string型の値をnumberのみを受け入れるrepeat関数に渡していることを警告しています。これは正しい警告です。つまり、最初にpaddingnumberかどうかを明示的にチェックしておらず、それがstringの場合の処理も行っていません。まさにそれをこれから行います。

ts
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
Try

これがほとんど面白みのないJavaScriptコードのように見えるなら、それはある意味要点です。追加したアノテーションを除けば、このTypeScriptコードはJavaScriptのように見えます。TypeScriptの型システムは、型安全性を確保するために無理をすることなく、典型的なJavaScriptコードをできるだけ簡単に記述できるようにすることを目指しています。

それほど多くないように見えるかもしれませんが、実際には内部で多くの処理が行われています。TypeScriptが静的型を使用してランタイム値を分析するのと同じように、`if/else`、条件付き三項演算子、ループ、真偽値チェックなど、JavaScriptのランタイム制御フロー構造に対して型分析を重ねており、これらはすべて型に影響を与える可能性があります。

ifチェック内で、TypeScriptは`typeof padding === "number"`を認識し、それを*型ガード*と呼ばれる特別な形式のコードとして理解します。 TypeScriptは、プログラムが実行可能なパスをたどり、特定の位置にある値の最も具体的な型を分析します。 これらの特別なチェック(*型ガード*と呼ばれます)と代入を調べ、宣言された型よりも具体的な型に型を絞り込むプロセスを*Narrowing(型絞り込み)*と呼びます。 多くのエディターでは、これらの型が変化する様子を観察することができ、例でもそれを行います。

ts
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
(parameter) padding: number
}
return padding + input;
(parameter) padding: string
}
Try

TypeScriptがNarrowingのために理解している構文はいくつかあります。

typeof型ガード

見てきたように、JavaScriptはランタイムに値の型に関する非常に基本的な情報を提供できるtypeof演算子をサポートしています。 TypeScriptは、これが特定の文字列セットを返すことを期待しています。

  • "string"
  • "number"
  • "bigint"
  • "boolean"
  • "symbol"
  • "undefined"
  • "object"
  • "function"

padLeftで見たように、この演算子は多くのJavaScriptライブラリで非常によく登場し、TypeScriptはそれを理解して異なるブランチで型を絞り込むことができます。

TypeScriptでは、typeofによって返される値に対するチェックは型ガードです。 TypeScriptは、typeofが異なる値に対してどのように動作するかをエンコードしているため、JavaScriptにおけるその癖のいくつかを知っています。 たとえば、上記のリストでは、typeofは文字列nullを返さないことに注意してください。 次の例をご覧ください。

ts
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) {
'strs' is possibly 'null'.18047'strs' is possibly 'null'.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
Try

printAll関数では、strsがオブジェクトかどうかをチェックして、それが配列型かどうかを確認しようとしています(ここで、配列はJavaScriptではオブジェクト型であることを再確認しておくと良いでしょう)。 しかし、JavaScriptでは、typeof nullは実際には` "object"`です! これは、歴史の unfortunate な事故の1つです。

十分な経験を持つユーザーは驚かないかもしれませんが、JavaScriptでこれに出くわしたことがある人は全員ではありません。 幸いなことに、TypeScriptは、strsが単なる`string[]`ではなく、`string[] | null`に絞り込まれたことを教えてくれます。

これは、「真偽値」チェックと呼ぶものへの良い導入となるかもしれません。

真偽値による絞り込み

「真偽値(Truthiness)」は辞書には載っていないかもしれませんが、JavaScriptでは非常によく耳にする言葉です。

JavaScriptでは、条件式、`&&`、`||`、`if`文、ブール否定(`!`)などで、あらゆる式を使用できます。例えば、`if`文は、条件が常に`boolean`型であることを期待しません。

ts
function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `There are ${numUsersOnline} online now!`;
}
return "Nobody's here. :(";
}
Try

JavaScriptでは、`if`のような構文は、まず条件を`boolean`に「型強制」して意味を理解し、結果が`true`か`false`かによって分岐を選択します。次のような値は、

  • 0
  • NaN
  • `""`(空文字列)
  • `0n`(`bigint`バージョンのゼロ)
  • null
  • undefined

すべて`false`に型強制され、その他の値は`true`に型強制されます。値を`boolean`に型強制するには、`Boolean`関数を実行するか、より短い二重ブール否定を使用します。(後者は、TypeScriptが狭いリテラルブール型`true`を推論するのに対し、前者は`boolean`型を推論するという利点があります。)

ts
// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true, value: true
Try

この動作を利用することは、特に`null`や`undefined`のような値を防ぐために、かなり一般的です。例として、`printAll`関数に使用してみましょう。

ts
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);
}
}
Try

`strs`が真偽値であるかどうかをチェックすることで、上記のエラーを取り除くことができました。これにより、少なくともコードを実行したときに発生する恐ろしいエラーを防ぐことができます。

txt
TypeError: null is not iterable

ただし、プリミティブ型に対する真偽値チェックは、エラーが発生しやすい場合があることに注意してください。例として、`printAll`を記述する別の方法を考えてみましょう。

ts
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);
}
}
}
Try

関数の本体全体を真偽値チェックで囲みましたが、これには微妙な欠点があります。空文字列の場合を正しく処理できなくなる可能性があります。

TypeScriptはここでは何の害もありませんが、JavaScriptに慣れていない場合は、この動作に注意する価値があります。TypeScriptは多くの場合、バグを早期に発見するのに役立ちますが、値に対して*何もしない*ことを選択した場合、過度に規範的になることなくTypeScriptができることは限られています。必要に応じて、linterを使用して、このような状況を確実に処理できます。

真偽値による絞り込みについての最後の注意点は、`!`によるブール否定は、否定された分岐から除外されるということです。

ts
function multiplyAll(
values: number[] | undefined,
factor: number
): number[] | undefined {
if (!values) {
return values;
} else {
return values.map((x) => x * factor);
}
}
Try

等価性による絞り込み

TypeScriptは、`switch`文や`===`、`!==`、`==`、`!=`のような等価チェックも使用して、型を絞り込みます。例えば

ts
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
}
}
Try

上記の例では、`x`と`y`が等しいことをチェックしたとき、TypeScriptはそれらの型も等しくなければならないことを認識していました。`string`は`x`と`y`の両方が取ることができる唯一の共通型であるため、TypeScriptは最初の分岐では`x`と`y`が`string`でなければならないことを認識しています。

特定のリテラル値(変数ではなく)に対するチェックも機能します。真偽値による絞り込みのセクションでは、誤って空文字列を処理しなかったためにエラーが発生しやすい`printAll`関数を記述しました。代わりに、`null`をブロックするための具体的なチェックを行うことができ、TypeScriptは依然として`strs`の型から`null`を正しく削除します。

ts
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
}
}
}
Try

JavaScriptの`==`と`!=`による緩やかな等価チェックも正しく絞り込まれます。慣れていない場合、`== null`かどうかをチェックすると、実際にはそれが具体的に値`null`であるかどうかだけでなく、それが潜在的に`undefined`であるかどうかもチェックします。`== undefined`にも同じことが当てはまります。値が`null`または`undefined`のいずれかであるかどうかをチェックします。

ts
interface Container {
value: number | null | undefined;
}
 
function multiplyValue(container: Container, factor: number) {
// Remove both 'null' and 'undefined' from the type.
if (container.value != null) {
console.log(container.value);
(property) Container.value: number
 
// Now we can safely multiply 'container.value'.
container.value *= factor;
}
}
Try

`in`演算子による絞り込み

JavaScriptには、オブジェクトまたはそのプロトタイプチェーンに名前付きのプロパティがあるかどうかを判断するための演算子があります。`in`演算子です。TypeScriptは、これを潜在的な型を絞り込む方法として考慮します。

たとえば、コード:`"value" in x`で、`"value"`は文字列リテラル、`x`は共用体型です。「true」分岐は、オプションまたは必須プロパティ`value`を持つ`x`の型を絞り込み、「false」分岐は、オプションまたは欠落しているプロパティ`value`を持つ型に絞り込みます。

ts
type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
 
return animal.fly();
}
Try

繰り返しますが、オプションのプロパティは、絞り込みのために両側に存在します。たとえば、人間は(適切な装備があれば)泳ぐことも飛ぶこともできるため、`in`チェックの両側に表示されるはずです。

ts
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
 
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
animal;
(parameter) animal: Fish | Human
} else {
animal;
(parameter) animal: Bird | Human
}
}
Try

`instanceof`による絞り込み

JavaScriptには、値が別の値の「インスタンス」であるかどうかをチェックするための演算子があります。より具体的には、JavaScriptでは、`x instanceof Foo`は、`x`の*プロトタイプチェーン*に`Foo.prototype`が含まれているかどうかをチェックします。ここでは深く掘り下げませんが、`new`で構築できるほとんどの値に役立ちます。ご想像のとおり、`instanceof`も型ガードであり、TypeScriptは`instanceof`によってガードされた分岐で絞り込みます。

ts
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
(parameter) x: Date
} else {
console.log(x.toUpperCase());
(parameter) x: string
}
}
Try

代入

前述したように、変数に代入すると、TypeScriptは代入の右側を見て、左側を適切に絞り込みます。

ts
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
Try

これらの各代入が有効であることに注意してください。最初の代入後に`x`の観測された型が`number`に変更されたとしても、`string`を`x`に代入することができました。これは、`x`の*宣言された型* - `x`が開始された型 - が`string | number`であり、代入可能性は常に宣言された型に対してチェックされるためです。

`boolean`を`x`に代入した場合、それが宣言された型の一部ではなかったため、エラーが発生します。

ts
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'.2322Type 'boolean' is not assignable to type 'string | number'.
 
console.log(x);
let x: string | number
Try

制御フロー解析

これまでは、TypeScriptが特定の分岐内でどのように絞り込むかの基本的な例をいくつか見てきました。ただし、すべての変数から上に移動して、`if`、`while`、条件式などで型ガードを探すだけではありません。例えば

ts
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
Try

`padLeft`は最初の`if`ブロック内から戻ります。TypeScriptはこのコードを分析し、`padding`が`number`の場合、本体の残りの部分(`return padding + input;`)に*到達できない*ことを確認できました。その結果、関数の残りの部分で`padding`の型から`number`を削除できました(`string | number`から`string`に絞り込み)。

到達可能性に基づくこのコードの分析は、*制御フロー分析*と呼ばれ、TypeScriptはこのフロー分析を使用して、型ガードと代入に遭遇したときに型を絞り込みます。変数が分析されると、制御フローは何度も分割と再マージを繰り返すことができ、その変数は各ポイントで異なる型を持つことが観測されます。

ts
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
}
Try

型述語の使用

これまでは、既存のJavaScript構文を使用して絞り込みを処理してきましたが、コード全体で型がどのように変化するかをより直接的に制御したい場合があります。

ユーザー定義の型ガードを定義するには、戻り値の型が*型述語*である関数を定義するだけです。

ts
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
Try

`pet is Fish`はこの例での型述語です。述語は`parameterName is Type`の形式を取り、`parameterName`は現在の関数シグネチャからのパラメーターの名前でなければなりません。

いくつかの変数で`isFish`が呼び出されるたびに、元の型が互換性がある場合、TypeScriptはその変数をその特定の型に*絞り込み*ます。

ts
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
 
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
Try

TypeScriptは、`if`分岐で`pet`が`Fish`であることを知っているだけではありません。`else`分岐では、`Fish`を*持っていない*ため、`Bird`を持っている必要があることも認識しています。

型ガード`isFish`を使用して、`Fish | Bird`の配列をフィルタリングし、`Fish`の配列を取得できます。

ts
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);
});
Try

さらに、クラスは`this is Type`を使用して型を絞り込むことができます。

アサーション関数

アサーション関数を使用して型を絞り込むこともできます。

判別可能なユニオン

これまでに見てきた例のほとんどは、`string`、`boolean`、`number` のような単純な型を持つ単一の変数を絞り込むことに焦点を当てていました。これはよくあることですが、JavaScript ではほとんどの場合、もう少し複雑な構造を扱うことになります。

動機付けとして、円や正方形のような形状をエンコードしようとしていると想像してみましょう。円は半径を追跡し、正方形は辺の長さを追跡します。どの形状を扱っているかを判断するために、`kind` というフィールドを使用します。`Shape` を定義する最初の試みは次のとおりです。

ts
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
Try

文字列リテラル型のユニオンを使用していることに注目してください。` "circle"` と `"square"` は、それぞれ円として扱うか正方形として扱うかを指示します。`string` の代わりに `"circle" | "square"` を使用することで、スペルミスの問題を回避できます。

ts
function handleShape(shape: Shape) {
// oops!
if (shape.kind === "rect") {
This comparison appears to be unintentional because the types '"circle" | "square"' and '"rect"' have no overlap.2367This comparison appears to be unintentional because the types '"circle" | "square"' and '"rect"' have no overlap.
// ...
}
}
Try

円を扱っているか正方形を扱っているかに基づいて適切なロジックを適用する `getArea` 関数を書くことができます。最初に円を処理してみましょう。

ts
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
'shape.radius' is possibly 'undefined'.18048'shape.radius' is possibly 'undefined'.
}
Try

`strictNullChecks` では、エラーが発生します。これは、`radius` が定義されていない可能性があるため適切です。しかし、`kind` プロパティで適切なチェックを実行した場合はどうでしょうか?

ts
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
'shape.radius' is possibly 'undefined'.18048'shape.radius' is possibly 'undefined'.
}
}
Try

うーん、TypeScript はまだここで何をすべきかわかりません。値について、型チェッカーよりも多くのことを知っているポイントに達しました。非 null アサーション(`shape.radius` の後の `!`)を使用して、`radius` が確実に存在することを示すことができます。

ts
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius! ** 2;
}
}
Try

しかし、これは理想的とは言えません。`shape.radius` が定義されていることを型チェッカーに納得させるために、これらの非 null アサーション(`!`)を少し叫ばなければなりませんでした。しかし、コードを移動し始めると、これらのアサーションはエラーを起こしやすくなります。さらに、`strictNullChecks` の外では、オプションのプロパティは読み取るときに常に存在すると見なされるため、これらのフィールドのいずれかに誤ってアクセスできます。間違いなく改善できます。

この `Shape` のエンコードの問題は、型チェッカーが `kind` プロパティに基づいて `radius` または `sideLength` が存在するかどうかを知る方法がないことです。 *私たち* が知っていることを型チェッカーに伝える必要があります。それを念頭に置いて、`Shape` を定義する別の方法を考えてみましょう。

ts
interface Circle {
kind: "circle";
radius: number;
}
 
interface Square {
kind: "square";
sideLength: number;
}
 
type Shape = Circle | Square;
Try

ここでは、`kind` プロパティの値が異なる 2 つの型に `Shape` を適切に分離しましたが、`radius` と `sideLength` はそれぞれの型で必須プロパティとして宣言されています。

`Shape` の `radius` にアクセスしようとするとどうなるか見てみましょう。

ts
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
Property 'radius' does not exist on type 'Shape'. Property 'radius' does not exist on type 'Square'.2339Property 'radius' does not exist on type 'Shape'. Property 'radius' does not exist on type 'Square'.
}
Try

`Shape` の最初の定義と同様に、これはまだエラーです。`radius` がオプションだった場合、TypeScript はプロパティが存在するかどうかを判断できなかったため、エラーが発生しました(`strictNullChecks` が有効になっている場合)。`Shape` がユニオンになったので、TypeScript は `shape` が `Square` である可能性があり、`Square` には `radius` が定義されていないことを示しています。どちらの解釈も正しいですが、`strictNullChecks` の構成方法に関係なく、`Shape` のユニオンエンコードのみがエラーを引き起こします。

しかし、`kind` プロパティをもう一度チェックしてみたらどうでしょうか?

ts
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
(parameter) shape: Circle
}
}
Try

これでエラーがなくなりました!ユニオンのすべての型にリテラル型を持つ共通のプロパティが含まれている場合、TypeScript はそれを *判別可能なユニオン* と見なし、ユニオンのメンバーを絞り込むことができます。

この場合、`kind` はその共通プロパティでした(これは `Shape` の *判別子* プロパティと見なされます)。`kind` プロパティが `"circle"` であるかどうかを確認すると、`kind` プロパティの型が `"circle"` でない `Shape` のすべての型が削除されました。これにより、`shape` は `Circle` 型に絞り込まれました。

同じチェックは `switch` ステートメントでも機能します。これで、厄介な `!` 非 null アサーションなしで完全な `getArea` を記述できます。

ts
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
(parameter) shape: Circle
case "square":
return shape.sideLength ** 2;
(parameter) shape: Square
}
}
Try

ここで重要なのは `Shape` のエンコードでした。TypeScript に正しい情報を伝えること、つまり `Circle` と `Square` は実際には特定の `kind` フィールドを持つ 2 つの別々の型であるということは非常に重要でした。そうすることで、そうでなければ書いていた JavaScript と変わらない型安全な TypeScript コードを書くことができます。そこから、型システムは「正しい」ことを行い、`switch` ステートメントの各ブランチの型を理解することができました。

余談ですが、上記の例を試してみて、いくつかの return キーワードを削除してみてください。`switch` ステートメントの異なる句に誤ってフォールスルーした場合に、型チェックがバグの回避に役立つことがわかります。

判別可能なユニオンは、円や正方形について話すだけではありません。ネットワーク経由でメッセージを送信する場合(クライアント/サーバー通信)、状態管理フレームワークでミューテーションをエンコードする場合など、JavaScript であらゆる種類のメッセージングスキームを表すのに適しています。

`never` 型

絞り込みを行うと、すべての可能性がなくなるまでユニオンのオプションを減らすことができます。そのような場合、TypeScript は `never` 型を使用して、存在しないはずの状態を表します。

網羅性チェック

`never` 型はすべての型に割り当てることができます。ただし、`never` にはどの型も割り当てることができません(`never` 自体を除く)。これは、絞り込みを使用し、`switch` ステートメントで網羅的なチェックを行うために `never` が現れることに依存できることを意味します。

たとえば、`getArea` 関数に、形状を `never` に割り当てようとする `default` を追加しても、すべてのケースが処理されている場合はエラーが発生しません。

ts
type Shape = Circle | Square;
 
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
Try

`Shape` ユニオンに新しいメンバーを追加すると、TypeScript エラーが発生します

ts
interface Triangle {
kind: "triangle";
sideLength: number;
}
 
type Shape = Circle | Square | Triangle;
 
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
Type 'Triangle' is not assignable to type 'never'.2322Type 'Triangle' is not assignable to type 'never'.
return _exhaustiveCheck;
}
}
Try

TypeScript のドキュメントはオープンソースプロジェクトです。プルリクエストを送信してこれらのページの改善にご協力ください ❤

このページの貢献者
RCRyan Cavanaugh (52)
OTOrta Therox (15)
SBSiarhei Bobryk (2)
ABAndrew Branch (2)
DRDaniel Rosenwasser (2)
26+

最終更新日: 2024年3月21日