TypeScriptの基礎 - 型の絞り込み
概要
型の絞り込み (Type Narrowing) とは、TypeScriptコンパイラが変数の型を より具体的な型に推論し直す 仕組みである。
ユニオン型のように複数の型を持つ変数に対して、条件分岐や型チェックを行うことで、その分岐内での型を安全に特定できる。
ユニオン型の定義については、TypeScriptの基礎 - ユニオン型のページを参照すること。
TypeScriptコンパイラは制御フロー解析 (Control Flow Analysis) により、if / else、switch、ループ等の構造を追跡して各ポイントでの型を自動的に絞り込む。
TypeScript 5.4以降ではクロージャ内での const 変数の型絞り込みが保持されるようになり、
TypeScript 5.5では配列の filter() メソッドに型ガード関数を渡した場合の型推論も改善された。
| 手法 | 概要 |
|---|---|
| 制御フロー解析 | コンパイラがコードの実行パスを分析して型を自動的に絞り込む仕組み |
typeof による絞り込み |
プリミティブ型を識別するための最も基本的な型ガード |
instanceof による絞り込み |
クラスインスタンスを識別するための型ガード |
in 演算子による絞り込み |
オブジェクトのプロパティ存在チェックを利用した絞り込み |
| 真偽値チェックによる絞り込み | ファルシー値を利用して null や undefined を除外する手法
|
| 等価チェックによる絞り込み | === / !== や == null を使った絞り込み
|
| カスタム型ガード関数 | is キーワードや asserts キーワードを使ったユーザ定義の型ガード
|
| 判別可能なユニオンの絞り込み | 共通のリテラル型プロパティを使ったswitch文による網羅的な絞り込み |
型の絞り込みを活用することにより、ランタイムエラーを防ぎ、型安全なコードを記述できる。
型アサーション (as) による強制的な型変換の代替手段として、型の絞り込みを優先的に使用することが推奨される。
型アサーションの詳細については、TypeScriptの基礎 - 型アサーションのページを参照すること。
制御フロー解析
TypeScriptコンパイラは、制御フロー解析 (Control Flow Analysis) によってコードの実行可能なパスを追跡し、各ポイントで変数の最も具体的な型を推論する。
if / else、switch文、ループ、条件演算子などの構造を考慮し、型ガードや条件式の結果に基づいて型を自動的に絞り込む。
制御フローが分岐・再合流する際には、各ブランチの型情報が個別に追跡される。
以下に制御フロー解析の基本的な例を示す。
function example(value: string | number | null) {
if (value === null) {
return; // この行を過ぎると、valueはnullでない
}
// ここでvalueの型 : string | number
if (typeof value === "string") {
console.log(value.length); // valueの型 : string
}
else {
console.log(value.toFixed(2)); // valueの型 : number
}
}
上記の例では、最初のif文でnullを除外した後、typeof チェックによりさらに string と number に絞り込まれている。
コンパイラはこの絞り込みを各ブランチで自動的に認識するため、各ブランチ内で型安全なメソッド呼び出しが可能となる。
typeofによる型の絞り込み
typeof 演算子は、JavaScriptの組み込み演算子であり、値の型を文字列として返す。
TypeScriptはこの戻り値を利用して、条件分岐内の型を絞り込む。
下表に、typeof が返す文字列の一覧を示す。
| 値の種類 | 返す文字列 |
|---|---|
| 文字列 | "string" |
| 数値 | "number" |
| 論理値 | "boolean" |
| undefined | "undefined" |
| オブジェクト (null含む) | "object" |
| 関数 | "function" |
| シンボル | "symbol" |
| 任意精度整数 | "bigint" |
基本的な使用方法
typeof を使用したプリミティブ型の絞り込みの例を以下に示す。
function printLength(str: string | number) {
if (typeof str === "string") {
console.log(str.length); // strはstring型
}
else {
console.log(str); // strはnumber型
}
}
typeof による絞り込みは、プリミティブ型 (string、number、boolean、bigint、symbol、undefined) の識別に適している。
typeofの注意点
typeof null === "object" となるのはJavaScriptの歴史的な仕様であり、null と通常のオブジェクトを区別するためには追加のチェックが必要である。
const value: null | object = null;
// 間違った型チェック
if (typeof value === "object") {
// ここでvalueがnullである可能性がある
}
// 正しい型チェック
if (typeof value === "object" && value !== null) {
console.log(Object.keys(value));
}
typeof でオブジェクト型を確認する時は、必ず value !== null を併用することを推奨する。
instanceofによる型の絞り込み
instanceof 演算子は、オブジェクトが特定のクラスのインスタンスであるかどうかを確認するために使用する。
クラスインスタンスを含むユニオン型の絞り込みに適している。
なお、instanceof はインターフェース型や型エイリアスに対しては使用できない。
インターフェースや型エイリアスはコンパイル後に消去されるため、ランタイムでの比較が不可能であることに注意する。
基本的な使用方法
2つのクラスを含むユニオン型に対して、instanceof で絞り込む例を以下に示す。
以下の例では、elseブランチでは Dog ではないことが確定しているため、TypeScriptは Cat 型として推論する。
class Dog {
bark() { console.log("Woof!"); }
}
class Cat {
meow() { console.log("Meow!"); }
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // animalはDog型
}
else {
animal.meow(); // animalはCat型
}
}
カスタムクラスでの使用
標準組み込みクラスを含む複数のクラスからなるユニオン型でも、instanceof を活用できる。
function processValue(value: Date | Error | RegExp) {
if (value instanceof Date) {
console.log(value.toISOString());
}
else if (value instanceof Error) {
console.log(value.message);
}
else if (value instanceof RegExp) {
console.log(value.source);
}
}
Date、Error、RegExp はいずれもJavaScriptの組み込みクラスであり、instanceof による絞り込みが可能である。
in演算子による型の絞り込み
in 演算子は、オブジェクトに特定のプロパティが存在するかどうかを確認するために使用する。
クラスのインスタンスではなく、インターフェースや型エイリアスで定義されたオブジェクト型の絞り込みに有効である。
基本的な使用方法
異なるプロパティを持つ型のユニオンに対して in 演算子で絞り込む例を以下に示す。
以下の例では、"swim" in animal が真の場合、TypeScriptは animal を Fish 型と推論する。
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // animalはFish型
}
else {
animal.fly(); // animalはBird型
}
}
オブジェクト型のユニオンでの活用
プリミティブ型とオブジェクト型が混在するユニオン型に対して、typeof と in を組み合わせることで段階的に絞り込める。
最初に typeof でプリミティブ型を除外して、その後 in でオブジェクト型のプロパティを確認することで、全ての型を正確に絞り込める。
function handleInput(input: string | { value: string } | { data: number[] }) {
if (typeof input === "string") {
console.log(input.toUpperCase());
}
else if ("value" in input) {
console.log(input.value);
}
else if ("data" in input) {
console.log(input.data.length);
}
}
真偽値チェックによる絞り込み
JavaScriptでは、条件式で評価された時に false と見なされる値をファルシー (Falsy) な値と呼ぶ。
TypeScriptはこの仕組みを利用して型を絞り込む。
| 値 |
|---|
| null |
| undefined |
| false |
| 0(および -0) |
| NaN |
| ""(空文字列) |
上記以外の値はトゥルーシー (Truthy) な値として扱われる。
truthiness narrowing
if 文の条件として変数を直接評価することで、ファルシーな値を除外できる。
以下の例では、if (str) の条件により、null と undefined が除外され、トゥルーシーな値のみが絞り込まれる。
// nullの除外
function printLength(str: string | null) {
if (str) {
console.log(str.length); // strはstring型 (nullは除外)
}
}
// null と undefinedの同時除外
function processValue(value: string | null | undefined) {
if (value) {
console.log(value.toUpperCase()); // valueはstring型
}
}
null / undefinedの除外
真偽値チェックでは、null や undefined だけでなく、空文字列 "" や 0 もファルシーとして除外される点に注意が必要である。
function handleInput(input: string | null) {
if (input) {
// 空文字列 "" は除外される
console.log(input);
}
// 空文字列を含めたい場合は !== null を使用する
if (input !== null) {
console.log(input); // "" も処理される
}
}
空文字列や 0 を有効な値として扱いたい場合は、真偽値チェックではなく等価チェック (!== null、!== undefined) を使用することを推奨する。
等価チェックによる絞り込み
等価演算子を使った比較によっても、TypeScriptは型を絞り込む。
厳密等価 (=== / !==) と緩やかな等価 (== / !=) の両方が型の絞り込みに活用できる。
=== / !==による絞り込み
厳密等価演算子を使用した比較では、両方の変数が等しい場合に共通する型のみが絞り込まれる。
以下の例では、xは string | number、yは string | boolean であり、共通する型はstringのみであるため、
x === y が成立する場合は両方ともstring型に絞り込まれる。
function compare(x: string | number, y: string | boolean) {
if (x === y) {
// xとyが等しい場合、共通する型はstringのみ
let t: string = x;
let u: string = y;
}
}
!== による絞り込みでは、特定の値を除外した残りの型が絞り込まれる。
function processStatus(status: "pending" | "success" | "error") {
if (status !== "error") {
// status は "pending" | "success" 型
console.log(status);
}
}
== null によるnullとundefinedの同時チェック
緩やかな等価演算子 == を使用すると、null と undefined を同時にチェックできる。
value == null は value === null || value === undefined と同等の意味を持つ。
function processValue(value: string | null | undefined) {
if (value == null) {
// null または undefined
console.log("値がありません");
}
else {
let t: string = value; // nullでもundefinedでもない
console.log(t.toUpperCase());
}
}
== null による同時チェックは、null と undefined を同等に扱いたい場面で簡潔に記述できる。
ただし、プロジェクトのlintルールによっては緩やかな等価演算子の使用が禁止されている場合があるため、プロジェクトの規約を確認すること。
カスタム型ガード関数
TypeScript組み込みの型ガード (typeof、instanceof、in) だけでは対応できない複雑な型チェックに対して、
カスタム型ガード関数を定義することができる。
isキーワードの構文
カスタム型ガード関数は、戻り値の型として <引数名> is <型> という型述語 (Type Predicate) を指定する。
関数が true を返した場合、呼び出し側のスコープで指定した型への絞り込みが行われる。
例えば、value is string という戻り値の型が型述語であり、この関数が true を返した場合にvalueがstring型であることをコンパイラに伝える。
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isNonNull<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
実用的な型ガード
カスタム型ガード関数は、配列の filter() メソッドと組み合わせると有用である。
通常の filter() ではフィルタ後の型が推論されないが、型ガード関数を渡すことで正確な型が推論される。
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isNonNull<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
const mixed: (string | number | null)[] = ["a", 1, null, "b", 2];
const strings = mixed.filter(isString); // stringsの型 : string[]
const nonNull = mixed.filter(isNonNull); // nonNullの型 : (string | number)[]
型ガード関数を filter() に渡すことにより、フィルタ後の配列の型が正確に推論される。
Assertion Functions (assertsキーワード)
アサーション関数は、型ガード関数の変形であり、値が期待する型でない場合に例外をスローする。
戻り値の型として asserts <引数名> is <型> を指定する。
型ガード関数との主な違いを以下に示す。
- 型ガード関数は true または false を返し、呼び出し側の条件分岐で型を絞り込む。
- アサーション関数は成功時に型を絞り込み、失敗時に例外をスローして関数を終了する。(return しない)
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Value is not a string");
}
}
function assertDefined<T>(value: T): asserts value is NonNullable<T> {
if (value === null || value === undefined) {
throw new Error("Value is null or undefined");
}
}
// 使用方法
const value: unknown = "hello";
assertIsString(value);
console.log(value.toUpperCase()); // value の型は自動的に string に絞られた
assertIsString(value) の呼び出し後、コンパイラはvalueがstring型であることを認識するため、それ以降の行でstring型のメソッドを安全に呼び出せる。
判別可能なユニオンの絞り込み
判別可能なユニオン (Discriminated Union) とは、共通のリテラル型プロパティ (判別プロパティ) を持つユニオン型である。
判別可能なユニオンの定義については、TypeScriptの基礎 - ユニオン型のページを参照すること。
以下の例では、判別プロパティを使用したswitch文による絞り込みを行っている。
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "triangle"; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
shape.kind という判別プロパティにより、各caseブランチで shape の型が自動的に絞り込まれる。
case "circle" ブランチでは shape.radius に、case "triangle" ブランチでは shape.base と shape.height にそれぞれアクセスできる。
switch文での絞り込みは、網羅性チェックと組み合わせることにより、将来的なケースの追加漏れをコンパイル時に検出できる。
以下の例では、defaultブランチで assertNever(status) を呼び出すことにより、全てのケースが処理されていることを型システムで保証できる。
仮に、Status型に新しい値が追加された場合、defaultブランチに到達した時点で assertNever の引数が never 型ではなくなりコンパイルエラーとなる。
type Status = "pending" | "success" | "error";
function assertNever(value: never): never {
throw new Error(`Unhandled value: ${value}`);
}
function handleStatus(status: Status): string {
switch (status) {
case "pending": return "Loading...";
case "success": return "Done!";
case "error": return "Error occurred";
default: return assertNever(status);
}
}
リデューサーパターンでの活用例を以下に示す。
以下の例では、action.type を判別プロパティとすることで、各caseブランチで対応するプロパティのみにアクセスできる。
type ReducerAction =
| { type: "add"; a: number; b: number }
| { type: "concat"; firstArr: any[]; secondArr: any[] }
| { type: "multiply"; x: number; y: number };
function reducer(action: ReducerAction): any {
switch (action.type) {
case "add": return action.a + action.b;
case "concat": return action.firstArr.concat(action.secondArr);
case "multiply": return action.x * action.y;
}
}
関連情報
- TypeScriptの基礎 - tsconfig.json
- TypeScriptの基礎 - 型注釈とプリミティブ型
- TypeScriptの基礎 - 型推論
- TypeScriptの基礎 - 型エイリアス
- TypeScriptの基礎 - オブジェクト型とインターフェース
- TypeScriptの基礎 - 関数の型定義
- TypeScriptの基礎 - ユニオン型
- TypeScriptの基礎 - 型アサーション