TypeScript 型判定 完全ガイド | typeof, instanceof, is演算子を解説

TypeScript 型判定 完全ガイド | typeof, instanceof, is演算子を徹底解説

TypeScriptは、JavaScriptに静的型付けの概念を導入することで、開発体験の向上、エラーの早期発見、コードの保守性向上に大きく貢献しています。コンパイル時にコードの型安全性Tチェックを行うことで、多くのバグを未然に防ぐことができます。

しかし、JavaScriptが実行されるランタイム環境では、TypeScriptの型情報は基本的に存在しません。実行時には、JavaScriptのプリミティブ型やオブジェクトといった概念のみが存在します。アプリケーションが動的なデータを扱う場合(例: ユーザーからの入力、外部APIからの応答、データベースからの取得データなど)、実行時にそのデータの「実際の」型を知り、それに応じた処理を行う必要が出てきます。

ここで重要になるのが「型判定」または「型ガード (Type Guard)」と呼ばれるテクニックです。型ガードは、実行時の特定の条件(例: 変数が文字列であるか、特定のクラスのインスタンスであるか、特定のプロパティを持つオブジェクトであるかなど)に基づいて、TypeScriptコンパイラに対してその変数の型をより狭い範囲に絞り込ませる(Narrowing)機能を提供します。これにより、実行時の動的な値に対しても、型安全なコードを書くことが可能になります。

この記事では、TypeScriptにおける型判定の主要なツールである typeofinstanceof、そして強力なユーザー定義型ガードを可能にする is 演算子について、その仕組み、使い方、具体的なコード例、そして限界や注意点まで、約5000語をかけて徹底的に解説します。さらに、関連する in 演算子、Array.isArray()、リテラル型ガード、アサーション関数など、実行時の型チェックに役立つ様々なテクニックも網羅します。この記事を読むことで、あなたはTypeScriptで動的な値を安全かつ効率的に扱うための強力なスキルを身につけることができるでしょう。

1. TypeScriptの型システムと実行時

TypeScriptの最大の利点は静的型付けです。私たちは変数や関数の引数、戻り値などに型アノテーションを付けることで、コードの意図を明確にし、コンパイル時に型エラーを発見できます。

“`typescript
let greeting: string = “Hello, world!”; // 静的に型を宣言
function add(a: number, b: number): number { // 引数と戻り値に型を宣言
return a + b;
}

// コンパイル時に型エラーを検出
// greeting = 123; // Error: Type ‘number’ is not assignable to type ‘string’.
// add(“1”, “2”); // Error: Argument of type ‘string’ is not assignable to parameter of type ‘number’.
“`

しかし、TypeScriptの型システムは主にコンパイル時に機能します。コードがJavaScriptにトランスパイルされる際、ほとんどの型アノテーションやインターフェース、型エイリアスといったTypeScript独自の構文は取り除かれます。

“`typescript
// 上記TypeScriptコードのコンパイル結果(JavaScript)
var greeting = “Hello, world!”;
function add(a, b) {
return a + b;
}

// 型情報は一切残っていない
“`

実行時のJavaScriptには、string, number, boolean, undefined, symbol, bigint といったプリミティブ型と、object, function といった参照型が存在するだけです。私たちがTypeScriptで定義したカスタム型(インターフェースやクラスではない型エイリアスなど)は、実行時には影も形もありません。

このコンパイル時と実行時の型の違いが、実行時の型判定が必要になる理由です。たとえば、any 型や unknown 型として受け取った値、あるいはユニオン型として受け取った値の「実際の」型を実行時に確認し、その型に応じた安全な処理を行うためには、JavaScriptの実行時機能やTypeScriptの型ガードの仕組みを利用する必要があります。

例えば、次のようなコードを考えてみましょう。

typescript
function processValue(value: string | number) {
// ここで value が string なのか number なのかを知りたい
// 知らないと安全に string のメソッドや number の演算子を使えない
// console.log(value.toUpperCase()); // Error if value is number
// console.log(value.toFixed(2)); // Error if value is string
}

このような状況で、typeofinstanceofis 演算子(型ガード)が役立ちます。これらは、実行時の条件に基づいてTypeScriptコンパイラに型の情報を与え、コードの特定のブロック内で変数の型をより具体的に推論させる(Narrowing)ためのツールです。

2. typeof 演算子による型判定

typeof 演算子は、JavaScriptに古くから存在する機能です。指定された式を評価し、そのオペランドの型を示す文字列を返します。TypeScriptは、このJavaScriptの typeof の結果を利用して、特定の条件下で変数の型を絞り込むことができます。

2.1 JavaScriptの typeof の挙動

JavaScriptの typeof 演算子は、以下のいずれかの文字列を返します。

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

注意が必要な点として、typeof null'object' を返します。これはJavaScriptの設計上の歴史的なバグとして知られていますが、互換性のために修正されていません。また、配列 ([]) や正規表現 (/regex/)、Dateオブジェクトなども typeof では 'object' と判定されます。

javascript
typeof undefined; // 'undefined'
typeof true; // 'boolean'
typeof 123; // 'number'
typeof 123n; // 'bigint'
typeof "hello"; // 'string'
typeof Symbol(); // 'symbol'
typeof {}; // 'object'
typeof []; // 'object'
typeof null; // 'object' <-- 注意
typeof function() {}; // 'function'
typeof new Date(); // 'object'
typeof /abc/; // 'object'

2.2 TypeScriptにおける typeof 型ガード (Type Guard)

TypeScriptでは、if 文や条件演算子の中で typeof variable === '...' という形式のチェックを行うと、コンパイラはJavaScriptの typeof が返す特定の文字列リテラルに基づいて、そのブロック内での variable の型をNarrowingします。

typeof 型ガードが有効な文字列リテラルは、JavaScriptの typeof が返す以下の7つです('object' を除く8つのうち、'object' 以外の7つです)。

  • 'string'
  • 'number'
  • 'boolean'
  • 'undefined'
  • 'symbol'
  • 'bigint'
  • 'function'

'object' は、typeof null'object' となるため、単独では特定の型(例えば非nullオブジェクト)にNarrowingするには不十分です。

具体的なコード例:

ユニオン型 string | number | boolean | undefined の変数の型を判定する例を見てみましょう。

“`typescript
function processInput(input: string | number | boolean | undefined | object) {
if (typeof input === ‘string’) {
// このブロック内では input の型は string に絞り込まれる
console.log(“Received a string:”, input.toUpperCase());
// input.toFixed(2); // Error: Property ‘toFixed’ does not exist on type ‘string’.
} else if (typeof input === ‘number’) {
// このブロック内では input の型は number に絞り込まれる
console.log(“Received a number:”, input.toFixed(2));
// input.toUpperCase(); // Error: Property ‘toUpperCase’ does not exist on type ‘number’.
} else if (typeof input === ‘boolean’) {
// このブロック内では input の型は boolean に絞り込まれる
console.log(“Received a boolean:”, !input);
} else if (typeof input === ‘undefined’) {
// このブロック内では input の型は undefined に絞り込まれる
console.log(“Received undefined.”);
} else {
// ここに来る可能性があるのは object 型
// typeof null も ‘object’ なので、まだ input は object | null
console.log(“Received something else (possibly object or null).”, input);

    // null のチェックが必要
    if (input !== null) {
       // ここで input は object に絞り込まれる (null ではないobject)
       console.log("Received a non-null object.");
       // 配列かどうかは typeof では判定できない
       // if (typeof input === 'object' && Array.isArray(input)) { ... } // 別途 Array.isArray が必要
    } else {
       // ここで input は null に絞り込まれる
       console.log("Received null.");
    }
}

}

processInput(“hello”);
processInput(123.456);
processInput(true);
processInput(undefined);
processInput({});
processInput([]);
processInput(null);
processInput(new Date());
“`

この例からわかるように、typeof 型ガードを使うことで、コンパイラは条件を満たすブロック内で変数の型を正しく推論し、型安全性を確保してくれます。

typeof 型ガードの限界:

typeof 型ガードはプリミティブ型(と function)の判定には非常に便利ですが、以下のような型には直接適用できません。

  1. カスタムのオブジェクト型: インターフェース (interface) や型エイリアス (type) で定義したオブジェクト型には使えません。これらはコンパイル後に消滅するため、実行時には typeof は常に 'object' を返します(あるいは 'function')。
  2. クラスのインスタンス: クラスもJavaScriptの実行時ではオブジェクトまたは関数として扱われるため、typeof は通常 'object' または 'function' を返します。特定のクラスのインスタンスであるかを判定するには、後述する instanceof を使う必要があります。
  3. 配列: typeof []'object' です。配列であることを判定するには、Array.isArray() という専用のメソッドを使うのが一般的です。
  4. null: typeof null'object' です。null かどうかを判定するには、=== null または == null (nullish check) を使う必要があります。

したがって、typeof は主に基本的なプリミティブ型の判定に使うツールであると理解しておくことが重要です。

3. instanceof 演算子による型判定

instanceof 演算子も、JavaScriptの実行時機能です。指定されたオブジェクトが、特定のコンストラクタのプロトタイプチェーン上に存在するかどうかを確認します。これは、オブジェクトが特定のクラスのインスタンスであるかを判定するためによく使われます。

3.1 JavaScriptの instanceof の挙動

object instanceof Constructor という形式で使います。左辺の object が、右辺の Constructorprototype プロパティが参照するオブジェクトを、そのプロトタイプチェーンのどこかに持っている場合に true を返します。

“`javascript
class Animal {}
class Dog extends Animal {}
class Cat {}

const dog = new Dog();
const cat = new Cat();
const obj = {};

dog instanceof Dog; // true
dog instanceof Animal; // true (プロトタイプチェーンを辿る)
dog instanceof Object; // true (Object は全てのオブジェクトの祖先)

cat instanceof Cat; // true
cat instanceof Animal; // false
cat instanceof Dog; // false

obj instanceof Object; // true
// obj instanceof Animal; // Error: Animal is not a constructor or function
“`

instanceof は、関数(コンストラクタとして使えるもの)に対しても使用できます。

“`javascript
function MyConstructor() {}
const instance = new MyConstructor();

instance instanceof MyConstructor; // true
instance instanceof Object; // true
“`

配列は組み込みの Array コンストラクタのインスタンスです。

javascript
[] instanceof Array; // true
[] instanceof Object; // true

3.2 TypeScriptにおける instanceof 型ガード (Type Guard)

TypeScriptでは、if (variable instanceof MyClass) という形式のチェックを行うと、コンパイラはtrueのブロック内で variable の型を MyClass にNarrowingします。これは、MyClass がJavaScriptのクラスとして実行時に存在することを前提としています。

具体的なコード例:

複数のクラスが絡むユニオン型の型判定に instanceof は非常に有効です。

“`typescript
class Circle {
constructor(public radius: number) {}
getArea(): number { return Math.PI * this.radius ** 2; }
}

class Square {
constructor(public sideLength: number) {}
getArea(): number { return this.sideLength ** 2; }
}

type Shape = Circle | Square;

function printArea(shape: Shape) {
if (shape instanceof Circle) {
// このブロック内では shape は Circle 型に Narrowing される
console.log(“Circle area:”, shape.getArea());
// console.log(shape.sideLength); // Error: Property ‘sideLength’ does not exist on type ‘Circle’.
} else if (shape instanceof Square) {
// このブロック内では shape は Square 型に Narrowing される
console.log(“Square area:”, shape.getArea());
// console.log(shape.radius); // Error: Property ‘radius’ does not exist on type ‘Square’.
} else {
// ここには到達しないはず(Shape は Circle | Square なので)
// ただし、外部からの入力など unknown な値の場合はここに来る可能性がある
console.error(“Unknown shape type”);
}
}

const myCircle: Shape = new Circle(5);
const mySquare: Shape = new Square(10);

printArea(myCircle);
printArea(mySquare);

// 組み込みクラスの例
function processDateOrError(value: Date | Error | string) {
if (value instanceof Date) {
// value は Date 型
console.log(“It’s a Date:”, value.toISOString());
} else if (value instanceof Error) {
// value は Error 型
console.log(“It’s an Error:”, value.message);
} else {
// value は string 型
console.log(“It’s a string:”, value.length);
}
}

processDateOrError(new Date());
processDateOrError(new Error(“Something went wrong”));
processDateOrError(“Just a string”);
“`

instanceof は、継承関係にあるクラスに対しても期待通りに動作します。

“`typescript
class Vehicle {
constructor(public manufacturer: string) {}
}

class Car extends Vehicle {
constructor(manufacturer: string, public model: string) { super(manufacturer); }
}

class Truck extends Vehicle {
constructor(manufacturer: string, public capacity: number) { super(manufacturer); }
}

type Transportation = Vehicle | Car | Truck;

function getDetails(item: Transportation) {
if (item instanceof Car) {
// item は Car 型
console.log(Car: ${item.manufacturer} ${item.model});
} else if (item instanceof Truck) {
// item は Truck 型
console.log(Truck: ${item.manufacturer} with capacity ${item.capacity} tons);
} else if (item instanceof Vehicle) {
// item は Vehicle 型 (Car や Truck ではない Vehicle)
console.log(Generic Vehicle: ${item.manufacturer});
} else {
// ここには到達しないはず
console.error(“Unknown transportation type”);
}
}

getDetails(new Car(“Toyota”, “Camry”));
getDetails(new Truck(“Volvo”, 20));
getDetails(new Vehicle(“Boeing”)); // Vehicle 型として判定される
“`

この例では、instanceof はプロトタイプチェーンを辿るため、CarTruckVehicle のインスタンスであることは真ですが、TypeScriptコンパイラはより具体的な型からチェックすることで、正確な型Narrowingを行います。上記の例のように、最も具体的な型から順番に instanceof チェックを行うのが一般的です。

instanceof 型ガードの限界:

instanceof はクラスインスタンスの判定に強力ですが、以下のような場合には使えません。

  1. インターフェースや型エイリアス: これらはコンパイル時に消滅するため、実行時には存在しません。したがって、variable instanceof MyInterface のようなチェックはできません。インターフェースに似た構造を持つオブジェクトの型を判定したい場合は、後述するユーザー定義型ガードや in 演算子を使う必要があります。
  2. 純粋なオブジェクトリテラル: instanceof はコンストラクタとの関係を見ますが、{} のようなオブジェクトリテラルは特定の名前付きコンストラクタのインスタンスとはみなされません(厳密には Object のインスタンスですが、これはあまり有用な情報にならないことが多いです)。特定の構造を持つオブジェクトであることを判定するには不向きです。
  3. 異なるRealm/iframe間: instanceof は、オブジェクトとコンストラクタが同じJavaScript Realm (例えばブラウザのウィンドウやiframe) で作成されていない場合、期待通りに動作しないことがあります。これは、それぞれのRealmが独自のグローバルオブジェクトと組み込みコンストラクタを持つためです。このような場合は、より堅牢な判定方法(例えば、特定のプロパティの存在チェックや、既知の識別子プロパティによる判別可能なユニオン型)を検討する必要があります。

4. ユーザー定義型ガード (is 演算子)

typeof はプリミティブ型、instanceof はクラスインスタンスの判定には有効ですが、これらだけではカバーできない多くのシナリオがあります。例えば、

  • ユニオン型 User | Admin のうち、isAdmin: boolean プロパティを持つオブジェクトが Admin であると判定したい。
  • オブジェクトが特定のプロパティ(例: name: string, age: number)の組み合わせを持っているかを判定したい。
  • より複雑なカスタムロジック(例: 数値の範囲、文字列のパターンマッチなど)に基づいて型を絞り込みたい。

このような、より柔軟でカスタムな型判定を行うために、TypeScriptは「ユーザー定義型ガード」の仕組みを提供しています。これは、関数の戻り値型に特別な is 演算子を使用することで実現されます。

4.1 is 演算子の構文

ユーザー定義型ガードは、以下のような特別な戻り値型を持つ関数として定義します。

typescript
function isTypeName(variable: any): variable is TypeName {
// variable が TypeName 型であるかを判定するロジック
// 判定結果を boolean で返す
// true なら variable は TypeName 型、false ならそうではないと推論される
// この関数は必ず boolean を返す必要がある
}

  • isTypeName: 型ガード関数の名前。慣習として is から始まることが多い。
  • variable: any: 判定したい変数。引数の型は判定したい変数の可能な最も広い型(例: unknown やユニオン型)を指定するのが一般的ですが、any もよく使われます。
  • variable is TypeName: これがユーザー定義型ガードの核心部分です。関数の戻り値型として parameterName is Type の形式で指定します。parameterName は関数の引数名(ここでは variable)である必要があり、Type はその引数が絞り込まれるべき型です。
  • 関数本体は、variableTypeName であるという判定ロジックを実装し、その結果を boolean で返します。

この関数を if 文などの条件式で使用すると、TypeScriptコンパイラは、関数が true を返すパスでは引数 variable の型が TypeName であると推論し、Narrowingを行います。

4.2 is 演算子の使い方と具体的なコード例

ユニオン型のメンバーを、プロパティの存在や値によって判別する典型的な例を見てみましょう。

例1: プロパティの存在によるユニオン型の判別

“`typescript
interface Cat {
name: string;
meow(): void;
}

interface Dog {
name: string;
bark(): void;
}

type Pet = Cat | Dog;

// ユーザー定義型ガード関数
function isCat(pet: Pet): pet is Cat {
// Cat 型にのみ存在する meow メソッドが関数であるかチェックする
// ‘in’ 演算子を使ってプロパティの存在をチェックすることも多い
// return (pet as any).meow !== undefined && typeof (pet as any).meow === ‘function’;
// または、より簡潔にプロパティの存在をチェック
return ‘meow’ in pet && typeof (pet as Cat).meow === ‘function’;
}

function isDog(pet: Pet): pet is Dog {
// Dog 型にのみ存在する bark メソッドが関数であるかチェックする
return ‘bark’ in pet && typeof (pet as Dog).bark === ‘function’;
}

function makeSound(pet: Pet) {
if (isCat(pet)) {
// このブロック内では pet は Cat 型に Narrowing される
pet.meow(); // Cat 型のメソッドが呼び出せる
// pet.bark(); // Error: Property ‘bark’ does not exist on type ‘Cat’.
} else if (isDog(pet)) {
// このブロック内では pet は Dog 型に Narrowing される
pet.bark(); // Dog 型のメソッドが呼び出せる
// pet.meow(); // Error: Property ‘meow’ does not exist on type ‘Dog’.
} else {
// Pet は Cat | Dog なので、ここには到達しないはず(理論上)
// ただし、unknown な値が渡された場合は考慮が必要
console.error(“Unknown pet type”);
}
}

const myCat: Pet = { name: “Tama”, meow: () => console.log(“Nyaa!”) };
const myDog: Pet = { name: “Pochi”, bark: () => console.log(“Wan!”) };

makeSound(myCat);
makeSound(myDog);

// Pet 型に含まれないオブジェクトを渡した場合(any/unknown 経由など)
// makeSound({ name: “Kero”, chirp: () => console.log(“Chirp!”) } as any); // エラーは出ないが、最後の else ブロックに来る
“`

この例では、isCat 関数と isDog 関数がユーザー定義型ガードとして機能しています。これらの関数は、引数のオブジェクトが期待するプロパティやメソッドを持っているかを実行時にチェックし、その結果を is Type という戻り値型でコンパイラに伝えています。

例2: オブジェクトが特定の構造を持つかの判定

特定のプロパティの存在だけでなく、そのプロパティの値の型もチェックすることで、より厳密な判定が可能です。

“`typescript
interface User {
id: number;
username: string;
}

interface Product {
id: number;
price: number;
}

type Item = User | Product;

function isUser(item: Item): item is User {
// item がオブジェクトであり、id は number、username は string であるかをチェック
return typeof item === ‘object’ && item !== null &&
‘id’ in item && typeof item.id === ‘number’ &&
‘username’ in item && typeof item.username === ‘string’;
// Product には username プロパティがないことを利用して判定
// あるいは、Userにのみ存在する他のプロパティがあればそれを使う
}

function isProduct(item: Item): item is Product {
// item がオブジェクトであり、id は number、price は number であるかをチェック
// User には price プロパティがないことを利用して判定
return typeof item === ‘object’ && item !== null &&
‘id’ in item && typeof item.id === ‘number’ &&
‘price’ in item && typeof item.price === ‘number’;
}

function processItem(item: Item | unknown) { // unknown も受け付けるように変更
if (isUser(item)) {
// item は User 型
console.log(User ID: ${item.id}, Username: ${item.username});
} else if (isProduct(item)) {
// item は Product 型
console.log(Product ID: ${item.id}, Price: ${item.price});
} else {
// User も Product も当てはまらない場合
console.log(“Unknown item type or invalid structure.”);
console.log(item); // unknown のまま
}
}

const user: User = { id: 1, username: “Alice” };
const product: Product = { id: 101, price: 19.99 };
const randomObj = { type: “unknown”, value: 123 };

processItem(user);
processItem(product);
processItem(randomObj);
processItem(null);
processItem(undefined);
processItem(“hello”);
“`

この例では、'prop' in obj 演算子と typeof を組み合わせて、オブジェクトが期待するプロパティを持ち、かつその型も一致するかをチェックしています。

'prop' in obj 演算子:

'prop' in obj はJavaScriptの演算子で、obj オブジェクト自身またはそのプロトタイプチェーン上に 'prop' という名前のプロパティが存在するかどうかを真偽値で返します。TypeScriptでは、これも型ガードとして機能し、if ('prop' in obj) のブロック内で obj の型に 'prop' プロパティを持つ可能性のある型だけを残すようにNarrowingします。これは、ユーザー定義型ガードの実装の中で非常によく利用されます。

“`typescript
interface A { a: string; }
interface B { b: number; }

type Union = A | B;

function processUnion(value: Union) {
if (‘a’ in value) {
// value は A 型に Narrowing される(b は持たないと推論される)
console.log(value.a);
// console.log(value.b); // Error
} else {
// value は B 型に Narrowing される(a は持たないと推論される)
console.log(value.b);
// console.log(value.a); // Error
}
}

processUnion({ a: “hello” });
processUnion({ b: 123 });
// processUnion({ a: “hello”, b: 123 }); // エラーにならないが、if ブロックに入る
“`

4.3 判別可能なユニオン型 (Discriminant Union) と is 演算子の使い分け

ユーザー定義型ガードは非常に強力ですが、ユニオン型が特定の構造を持つ場合、型ガード関数を自分で書かなくてもTypeScriptが自動的に型をNarrowingしてくれる便利な仕組みがあります。それが「判別可能なユニオン型 (Discriminant Union)」です。

判別可能なユニオン型は、ユニオンを構成するすべてのメンバー型が、共通の「タグ」プロパティ(通常は文字列リテラル型を持つプロパティ)を持っている場合に成立します。

例: 判別可能なユニオン型

“`typescript
interface SuccessResult {
status: “success”; // タグプロパティ (リテラル型)
data: any;
}

interface ErrorResult {
status: “error”; // タグプロパティ (異なるリテラル型)
message: string;
}

type ApiResult = SuccessResult | ErrorResult;

function handleResult(result: ApiResult) {
// status プロパティの値をチェックすることで、TypeScriptが自動的に型をNarrowingする
if (result.status === “success”) {
// このブロック内では result は SuccessResult 型に Narrowing される
console.log(“Success! Data:”, result.data);
// console.log(result.message); // Error: Property ‘message’ does not exist on type ‘SuccessResult’.
} else { // result.status === “error”
// このブロック内では result は ErrorResult 型に Narrowing される
console.log(“Error! Message:”, result.message);
// console.log(result.data); // Error: Property ‘data’ does not exist on type ‘ErrorResult’.
}
}

// switch 文でも同様に機能する
function handleResultSwitch(result: ApiResult) {
switch (result.status) {
case “success”:
// result は SuccessResult 型
console.log(“Success (switch)! Data:”, result.data);
break;
case “error”:
// result は ErrorResult 型
console.log(“Error (switch)! Message:”, result.message);
break;
default:
// exhaustiveness checking のために never 型を使うことがある
const _exhaustiveCheck: never = result;
return _exhaustiveCheck; // これがないと、将来新しいタグを追加した場合にコンパイルエラーにならない
}
}

const success: ApiResult = { status: “success”, data: { id: 123 } };
const error: ApiResult = { status: “error”, message: “Operation failed.” };

handleResult(success);
handleResult(error);
handleResultSwitch(success);
handleResultSwitch(error);
“`

判別可能なユニオン型は、特にAPIレスポンスやイベントハンドリングなどで、複数の異なる形状のオブジェクトが同じユニオン型として扱われる場合に非常に有効です。タグプロパティの値(リテラル型)をチェックするだけで、TypeScriptが自動的に正確な型Narrowingを行ってくれるため、個別にユーザー定義型ガード関数を定義する手間が省けます。

is 演算子と判別可能なユニオン型の使い分け:

  • 判別可能なユニオン型:

    • ユニオンを構成するメンバー型が、共通のタグプロパティ(リテラル型を持つ)を持っている場合に最適。
    • シンプルで読みやすいコードになることが多い(if/else ifswitch 文でのタグプロパティチェック)。
    • TypeScriptが自動で型Narrowingしてくれるため、型ガード関数を自作する必要がない。
    • 型を追加した場合の exhaustiveness checking (網羅性チェック) が容易。
  • ユーザー定義型ガード (is):

    • ユニオン型に共通のタグプロパティがない場合。
    • 型判定のロジックがより複雑で、プロパティの存在チェックや値の型チェックだけでは不十分な場合(例: 特定の正規表現にマッチするか、数値の範囲内かなど)。
    • クラスインスタンスではないオブジェクト(インターフェースなど)の構造を判定したい場合。
    • 特定の「振る舞い」(特定のメソッドを持つかなど)で型を判別したい場合。

多くの場合、もしユニオン型を設計できる立場にあるなら、判別可能なユニオン型として設計することを検討すると、型判定のコードがよりシンプルになる可能性があります。しかし、外部ライブラリの型や既存のデータ構造を扱う場合など、判別可能なユニオン型にできないケースでは、ユーザー定義型ガードが強力なツールとなります。

5. その他の型判定/型チェックに関連するテクニック

typeofinstanceof、ユーザー定義型ガード(is)が主要な型判定のツールですが、TypeScriptには他にも型Narrowingに役立つ様々な機能やJavaScriptの実行時チェックを活用するテクニックがあります。

5.1 in 演算子によるプロパティの存在チェック (Property Narrowing)

前述のユーザー定義型ガードの例でも登場しましたが、JavaScriptの 'propertyName' in object 演算子は、TypeScriptでも型ガードとして機能します。ユニオン型で、片方の型にのみ存在するプロパティをチェックすることで、型をNarrowingできます。

“`typescript
interface Car { drive(): void; }
interface Ship { sail(): void; }

type Vehicle = Car | Ship;

function navigate(vehicle: Vehicle) {
if (‘drive’ in vehicle) {
// vehicle は Car 型に Narrowing される
vehicle.drive();
} else {
// vehicle は Ship 型に Narrowing される
vehicle.sail();
}
}

navigate({ drive: () => console.log(“Driving…”) });
navigate({ sail: () => console.log(“Sailing…”) });
“`

これは判別可能なユニオン型に似ていますが、タグプロパティがリテラル型である必要はなく、単にプロパティが存在するかどうかで判別します。ただし、プロパティの値が undefined である可能性を完全に排除できるわけではない点に注意が必要です(プロパティが存在しても値が undefined の場合がある)。より厳密には、プロパティの存在チェックと同時に値の型チェックも行う(ユーザー定義型ガードでよく行うように)方が安全な場合が多いです。

5.2 Array.isArray() による配列判定

JavaScriptで配列かどうかを判定する際には、typeof [] === 'object' という挙動から typeof は使えません。推奨される方法は Array.isArray() 静的メソッドを使用することです。TypeScriptはこのメソッドも型ガードとして認識します。

“`typescript
function processList(list: string | string[] | number) {
if (Array.isArray(list)) {
// list は string[] 型に Narrowing される
console.log(“It’s an array:”, list.join(“, “));
} else if (typeof list === ‘string’) {
// list は string 型
console.log(“It’s a string:”, list.toUpperCase());
} else {
// list は number 型
console.log(“It’s a number:”, list.toFixed(0));
}
}

processList([“apple”, “banana”]);
processList(“cherry”);
processList(100);
“`

5.3 リテラル型ガード (Equality Narrowing)

特定の変数がある特定のリテラル値と等しいかどうかをチェックすることでも、型をNarrowingできます。これは特に、文字列リテラル型や数値リテラル型などのユニオン型に対して有効です。

“`typescript
type Status = “idle” | “loading” | “success” | “error”;

function handleStatus(status: Status) {
if (status === “loading”) {
// status は “loading” 型に Narrowing される
console.log(“Loading data…”);
} else if (status === “success”) {
// status は “success” 型に Narrowing される
console.log(“Data loaded successfully.”);
} else {
// status は “idle” | “error” 型に Narrowing される
// さらに細かく判定したい場合は、もう一段チェックが必要
if (status === “error”) {
// status は “error” 型
console.log(“Error occurred.”);
} else {
// status は “idle” 型
console.log(“Idle state.”);
}
}
}

handleStatus(“loading”);
handleStatus(“success”);
handleStatus(“error”);
handleStatus(“idle”);
“`

if (variable === someValue)if (variable !== someValue) といった比較は、TypeScriptコンパイラにその時点での変数の型に関する強いヒントを与え、型 Narrowing を促進します。

5.4 Nullish Check (Equality Narrowing with null / undefined)

JavaScriptの == null 演算子は、値が null または undefined の場合に true を返します(nullish check)。TypeScriptでは、このチェックも型ガードとして機能し、nullundefined の可能性を排除して型をNarrowingするのに非常に便利です。

“`typescript
function greet(name: string | null | undefined) {
if (name == null) { // name === null || name === undefined と同じ
// このブロック内では name は null | undefined に Narrowing される
console.log(“Hello, Guest!”);
} else {
// このブロック内では name は string に Narrowing される
console.log(“Hello, ” + name.toUpperCase()); // string のメソッドが使える
}
}

greet(“Alice”);
greet(null);
greet(undefined);

// !== null や !== undefined も型ガードになる
function processOptionalValue(value: string | number | undefined) {
if (value !== undefined) {
// value は string | number に Narrowing される
console.log(“Value is defined:”, value);
if (typeof value === ‘string’) {
// value は string
console.log(value.length);
} else {
// value は number
console.log(value.toFixed(2));
}
} else {
// value は undefined
console.log(“Value is undefined.”);
}
}

processOptionalValue(“hello”);
processOptionalValue(123);
processOptionalValue(undefined);
“`

5.5 真偽値による型絞り込み (Truthiness Narrowing)

JavaScriptでは、0, "" (空文字列), null, undefined, NaN, false は Falsy 値と呼ばれ、Booleanコンテキスト(if 文の条件など)で false と評価されます。それ以外の値は Truthy 値と呼ばれ、true と評価されます。TypeScriptは、このTruthiness/Falsinessを利用して型をNarrowingすることがあります。

“`typescript
function processString(input: string | null | undefined) {
if (input) {
// input が Truthy (空文字列 “” ではない、null ではない、undefined ではない string) の場合
// input は string に Narrowing される
console.log(“Input is a non-empty string:”, input.length);
} else {
// input が Falsy ( “” または null または undefined の場合)
// input は “” | null | undefined に Narrowing される
console.log(“Input is empty or nullish.”);
console.log(“Falsy value:”, input);
}
}

processString(“hello”);
processString(“”); // else ブロック
processString(null); // else ブロック
processString(undefined); // else ブロック

function processNumber(input: number | null | undefined) {
if (input) {
// input が Truthy (0 ではない、null ではない、undefined ではない number) の場合
// input は number に Narrowing される
console.log(“Input is a non-zero number:”, input.toFixed(2));
} else {
// input が Falsy ( 0 または null または undefined または NaN の場合)
// input は 0 | null | undefined | NaN に Narrowing される
console.log(“Input is zero, nullish, or NaN.”);
console.log(“Falsy value:”, input);
}
}

processNumber(10);
processNumber(0); // else ブロック
processNumber(null); // else ブロック
processNumber(undefined); // else ブロック
processNumber(NaN); // else ブロック
“`

Truthiness Narrowing は便利ですが、0"" (空文字列) もFalsyになる点に注意が必要です。これらの値を有効な入力として扱いたい場合は、typeof チェックや == null チェックなど、より具体的な型判定を行う必要があります。

5.6 Assertion Functions (アサーション関数)

TypeScript 3.7で導入されたAssertion Functionsは、関数の呼び出し元に対して、特定の条件が満たされていること、あるいは引数が特定の型であることを保証するための新しい方法です。これは、条件が満たされない場合に必ずエラーを投げる関数として定義されます。

アサーション関数には主に2つの形式があります。

  1. asserts condition: 条件が満たされなかった場合、その関数より後ろのコードブロックでは condition が真であるとコンパイラに伝えます。
  2. asserts parameterName is Type: 関数が正常に完了した場合(エラーを投げなかった場合)、parameterNameType であるとコンパイラに伝えます。

これは、例えば入力値が nullundefined でないことを保証したり、特定の型であることを保証したりする場面で、コードの流れを中断せずに型安全性を向上させるのに役立ちます。

例: asserts parameterName is Type

“`typescript
interface Person {
name: string;
age: number;
}

// Assertion Function 定義
function assertIsPerson(value: any): asserts value is Person {
if (typeof value !== ‘object’ || value === null) {
throw new Error(“Value must be an object.”);
}
if (!(‘name’ in value) || typeof value.name !== ‘string’) {
throw new Error(“Value must have a ‘name’ property of type string.”);
}
if (!(‘age’ in value) || typeof value.age !== ‘number’) {
throw new Error(“Value must have an ‘age’ property of type number.”);
}
}

function processValue(value: unknown) {
// assertIsPerson を呼び出す
// これがエラーを投げなければ、この行より下では value は Person 型であると保証される
assertIsPerson(value);

// ここでは value は Person 型として扱える
console.log(`Processing person: ${value.name}, ${value.age}`);
// console.log(value.address); // Error: Property 'address' does not exist on type 'Person'.

}

processValue({ name: “Bob”, age: 30 }); // 正常終了
// processValue({ name: “Charlie” }); // ‘age’ プロパティがないためエラー発生
// processValue(“David”); // オブジェクトではないためエラー発生
“`

例: asserts condition

Nullish チェックにアサーション関数を使う例です。

“`typescript
function assertIsDefined(value: T): asserts value is NonNullable {
if (value === undefined || value === null) {
throw new Error(“Value must not be null or undefined.”);
}
// 関数が正常に完了すれば、value は NonNullable 型 (T から null と undefined を除いた型) と推論される
}

function printLength(text: string | null | undefined) {
assertIsDefined(text); // text が null/undefined でないことを保証

// ここでは text は string に Narrowing される
console.log(`Text length: ${text.length}`);

}

printLength(“hello”); // 正常終了
// printLength(null); // エラー発生
// printLength(undefined); // エラー発生
“`

Assertion Functions は、特定の前提条件(型に関するものを含む)が満たされていることをコードで表現し、コンパイラにその情報を伝える強力なツールです。通常の型ガードが if 文のブロック内でのNarrowingに使うのに対し、アサーション関数は関数呼び出しより後ろのコード全体に対して型を保証する点が異なります。

6. 型判定のベストプラクティスと注意点

TypeScriptで型判定を行う際に考慮すべきベストプラクティスといくつかの注意点があります。

  1. 静的型付けを最大限に活用する: 実行時判定は、コンパイル時には型が確定できない動的な値を扱う場合に必要になります。可能であれば、より厳密な型定義や設計によって、実行時判定の必要性を減らすことを目指しましょう。例えば、不明確なユニオン型よりも、判別可能なユニオン型を積極的に利用するなどです。
  2. typeof はプリミティブ型と関数に、instanceof はクラスインスタンスに: これらは組み込みの、効率的なチェック方法です。対象がこれらの型であれば、まずこれらを検討しましょう。
  3. カスタム型、インターフェース、複雑な構造にはユーザー定義型ガードまたは判別可能なユニオン型を: これらはより柔軟な判定ロジックを表現できます。どちらを選ぶかは、前述の使い分けのセクションを参考にしてください。
  4. nullundefined の扱いに注意: typeof null === 'object' は落とし穴です。Nullish check (== null または !== null) や Assertion Functions を活用して、これらを正確に扱えるようにしましょう。Optional Chaining (?.) や Nullish Coalescing (??) 演算子も、これらの値を安全に扱うために非常に役立ちます。
  5. 配列の判定には Array.isArray() を推奨: typeof は配列に対して 'object' を返してしまうため、配列判定には Array.isArray() を使うのが安全で意図も明確になります。
  6. 型アサーション (as Type) は最後の手段: variable as MyType という形式の型アサーションは、コンパイラに対して「この変数はあなたが知らないかもしれないけど、確実に MyType 型だよ」と伝えるものです。これはコンパイル時の型チェックをバイパスするため、もし実際には MyType 型でなかった場合、実行時にエラーが発生したり、予期しない挙動を引き起こしたりする可能性があります。型アサーションは、コンパイラが型を推論できないが開発者には確信がある場合にのみ慎重に使いましょう。可能な限り、型判定(型ガード)によってコンパイラに型を推論させる方が安全です。
  7. 実行時チェックの網羅性を意識する: ユーザー定義型ガードを書く際は、対象となる型を網羅的にチェックするロジックになっているかを確認しましょう。特に、外部からの入力やAPIレスポンスなど、信頼できないデータを扱う場合は、予期しない値が入ってくる可能性を考慮し、堅牢なチェックが必要です。例えば、プロパティの存在だけでなくその値の型もチェックするなどです。
  8. コントロールフロー分析 (Control Flow Analysis): TypeScriptコンパイラは、if/elseswitch、ループ、Truthinessチェック、Equalityチェック、typeofinstanceof、ユーザー定義型ガードなどの制御フローを分析し、コードの各ポイントで変数の型をNarrowingします。このコンパイラの挙動を理解することは、なぜ特定の場所で変数が特定の型として扱えるのかを理解する上で役立ちます。

7. まとめ

TypeScriptにおける型判定は、静的型付けされたコードとJavaScriptの動的な実行時環境との橋渡しをする重要な技術です。

  • typeof: プリミティブ型 (string, number, boolean, undefined, symbol, bigint) と function の実行時判定に利用でき、TypeScriptコンパイラによる型Narrowingを可能にします。ただし、typeof null === 'object' やオブジェクト型、クラスインスタンスの判定には限界があります。
  • instanceof: オブジェクトが特定のクラスのインスタンスであるかを実行時に判定し、プロトタイプチェーンに基づいて型Narrowingを行います。インターフェースや純粋なオブジェクトリテラルの判定には使えません。
  • ユーザー定義型ガード (is): function name(arg): arg is Type { ... } という構文で定義するカスタムの型判定関数です。typeofinstanceof では判定できない複雑な型構造やカスタムロジックに基づいた型Narrowingを可能にします。オブジェクトのプロパティの存在チェック ('prop' in obj) と組み合わせてよく利用されます。
  • 判別可能なユニオン型: 共通のタグプロパティを持つユニオン型では、タグプロパティの値による if/else ifswitch 文でのチェックによって、型ガード関数を自作することなくTypeScriptが自動的に型Narrowingを行います。これは、ユーザー定義型ガードの強力な代替手段となり得ます。
  • その他の関連テクニック: 'in' 演算子によるプロパティ Narrowing、Array.isArray() による配列判定、リテラル値との比較によるEquality Narrowing、Truthiness Narrowing、そしてAssertion Functionsなど、様々な方法で実行時の値に基づいて型を絞り込むことができます。

これらの型判定のツールを適切に使い分けることで、TypeScriptの最大の利点である型安全性を、実行時の動的なシナリオにおいても維持することができます。これにより、より信頼性が高く、保守しやすいJavaScriptアプリケーションを構築することが可能になります。

静的型付けによるコンパイル時チェックと、型ガードによる実行時チェックを組み合わせることで、TypeScriptのパワーを最大限に引き出し、堅牢なコード開発を実現しましょう。この記事が、あなたのTypeScriptにおける型判定の理解を深め、日々のコーディングに役立つことを願っています。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール