【TypeScript】instanceofで安全な型判別を実現する方法

【TypeScript】instanceofで安全な型判別を実現する方法 – 詳細解説

はじめに

ソフトウェア開発において、型の安全性は非常に重要な要素です。特に大規模なアプリケーションや、複数の開発者が関わるプロジェクトでは、型システムがコードの品質を保ち、バグを早期に発見する強力な助けとなります。TypeScriptは、JavaScriptに静的型付けを導入することで、この型安全性を大幅に向上させてくれます。

しかし、TypeScriptの型情報は基本的にコンパイル時にのみ存在し、実行時には失われます(型消去 – Type Erasure)。JavaScriptの実行環境では、変数やオブジェクトの「型」を実行時に知る必要がある場面がしばしば発生します。例えば、関数が取りうる複数の型の値を引数として受け取る場合や、外部からのデータ(APIレスポンス、ユーザー入力など)の構造を確認する場合などです。

このような「実行時の型判別」を行うために、TypeScriptはJavaScriptの既存の仕組みを活かしつつ、独自の「型ガード (Type Guards)」という概念を提供しています。型ガードは、特定のチェックが成功した場合に、TypeScriptコンパイラがそのスコープ内の変数の型をより狭い型に絞り込む(ナローイング – Narrowing)ことができる特殊な式や関数です。

JavaScriptに古くから存在する演算子であるinstanceofも、TypeScriptにおいては強力な型ガードとして機能します。これは、あるオブジェクトが特定のクラスのインスタンスであるかどうかを実行時に判定するために使用されます。TypeScriptコンパイラは、instanceofチェックが成功したブロック内で、検査対象の変数の型を、指定されたクラス型にナローイングしてくれます。

この記事では、TypeScriptにおけるinstanceof演算子を用いた安全な型判別について、その基本的な使い方から、複雑なシナリオでの応用、潜む落とし穴、そして他の型判別手法との比較まで、詳細に解説します。instanceofを効果的に使いこなし、より堅牢で読みやすいTypeScriptコードを書くための知識を深めましょう。

TypeScriptにおける型判別とは?

TypeScriptの主要な目的の一つは、開発プロセスのできるだけ早い段階、つまりコンパイル時に型エラーを検出することです。これにより、実行時エラーの発生を防ぎ、開発効率を高めることができます。しかし、現実のアプリケーションは、ユーザーの操作、ネットワーク通信、ファイル操作など、実行時に初めてその正確な構造が明らかになるデータを扱う必要があります。

例えば、関数が string | number 型の引数を受け取る場合、その引数が文字列なのか数値なのかによって処理を変えたいことがあります。コンパイル時にはこの引数の「型」は string | number というユニオン型ですが、実行時には具体的な string または number の値になります。JavaScriptのコードとして、この実行時の値がどちらのプリミティブ型であるかを確認し、それに応じた処理を実行する必要があります。

このような、実行時に変数の具体的な型や構造を判断し、TypeScriptコンパイラに対してその情報を伝える仕組みが「型判別 (Type Assertion / Type Narrowing)」です。型判別を行うための構文や関数が「型ガード (Type Guards)」と呼ばれます。

TypeScriptには、いくつかの組み込みの型ガードがあります。

  • typeof 演算子: プリミティブ型 (string, number, boolean, symbol, undefined, bigint) に対して使用し、その値の型名の文字列を返します。TypeScriptは typeof v === 'string' のようなチェックを型ガードとして認識し、チェックが成功したスコープ内で v の型を string にナローイングします。
  • instanceof 演算子: オブジェクトが特定のクラスのインスタンスであるかどうかをチェックします。TypeScriptは obj instanceof MyClass のようなチェックを型ガードとして認識し、チェックが成功したスコープ内で obj の型を MyClass にナローイングします。
  • in 演算子: オブジェクトが特定のプロパティを持っているかをチェックします。TypeScriptは 'propertyName' in obj のようなチェックも特定の状況で型ガードとして使用できます(主にユニオン型内のオブジェクトに対して、特定のプロパティの存在で判別する場合)。
  • ユーザー定義型ガード: 開発者が独自の型判別ロジックを持つ関数を定義し、その戻り値の型アノテーションに parameterName is Type という形式(型述語 – Type Predicate)を使用することで、TypeScriptコンパイラにその関数の成功が特定の型へのナローイングを保証することを伝える仕組みです。これは、インターフェースのような実行時には存在しない型や、より複雑な構造を持つオブジェクトの型判別に非常に強力です。

この記事では、これらの型ガードの中でも特に instanceof に焦点を当てて詳細に見ていきます。

instanceof演算子の基本

instanceof演算子は、JavaScriptの実行時機能であり、指定したオブジェクトが、指定したコンストラクタのprototypeプロパティをそのプロトタイプチェーン内に持っているかどうかをチェックします。言い換えれば、あるオブジェクトが特定のクラス(またはコンストラクタ関数)から生成されたインスタンスであるかを確認するための演算子です。

基本的な構文は以下の通りです。

javascript
object instanceof constructor

  • object: 検査対象のオブジェクト。
  • constructor: クラス(またはコンストラクタ関数)。

この式は、objectのプロトタイプチェーンを遡っていき、いずれかのプロトタイプがconstructor.prototypeと厳密等価(===)であればtrueを返し、そうでなければfalseを返します。

例えば、JavaScriptで以下のようなコードを考えてみましょう。

“`javascript
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}

class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
}

const animal = new Animal(“Generic Animal”);
const dog = new Dog(“Buddy”, “Golden Retriever”);
const str = “hello”;

console.log(animal instanceof Animal); // true
console.log(dog instanceof Animal); // true (Dog inherits from Animal)
console.log(dog instanceof Dog); // true
console.log(str instanceof String); // false (string primitive)
console.log(new String(“world”) instanceof String); // true (String object wrapper)
console.log(null instanceof Object); // false
console.log(undefined instanceof Object); // false
// console.log({} instanceof undefined); // TypeError
// console.log({} instanceof null); // TypeError
“`

重要な点は、instanceofはプロトタイプチェーンを辿るということです。DogクラスはAnimalクラスを継承しているため、dogオブジェクトのプロトタイプチェーンにはDog.prototypeAnimal.prototypeが含まれます。したがって、dog instanceof Animaltrueとなります。

プリミティブ型(文字列リテラル、数値リテラルなど)に対してinstanceofを使用すると、通常はfalseを返します。これは、プリミティブ値はオブジェクトではないため、プロトタイプチェーンを持たないからです(ただし、一時的にオブジェクトラッパーに変換されることがありますが、instanceofはオブジェクトそのものをチェックします)。明示的にオブジェクトラッパー(例: new String("hello"))を生成した場合はinstanceoftrueを返しますが、これは一般的な使い方ではありません。

instanceofの右側にnullまたはundefinedを指定すると、TypeErrorが発生します。検査対象のオブジェクトがnullまたはundefinedの場合、instanceoffalseを返します。

TypeScriptにおけるinstanceofと型ガード

TypeScriptは、このJavaScriptのinstanceof演算子を型ガードとして利用します。コンパイラは、if (obj instanceof MyClass) のような条件式があると、そのifブロックの内部では変数objの型を MyClass 型に絞り込むと推論します。これにより、開発者は安全にobjMyClassのインスタンスであると仮定したプロパティやメソッドにアクセスできるようになります。

例:

“`typescript
class Car {
drive() { console.log(“Driving a car”); }
}

class Truck {
load() { console.log(“Loading a truck”); }
}

type Vehicle = Car | Truck;

function operateVehicle(vehicle: Vehicle) {
// コンパイル時エラー: Property ‘drive’ does not exist on type ‘Vehicle’.
// vehicle.drive();

if (vehicle instanceof Car) {
// このブロック内では、TypeScriptは vehicle の型が Car であると推論
vehicle.drive(); // OK
// コンパイル時エラー: Property ‘load’ does not exist on type ‘Car’.
// vehicle.load();
} else {
// else ブロックでは、Vehicle から Car を除外した型、すなわち Truck と推論
vehicle.load(); // OK
// コンパイル時エラー: Property ‘drive’ does not exist on type ‘Truck’.
// vehicle.drive();
}
}

const myCar = new Car();
const myTruck = new Truck();

operateVehicle(myCar); // 出力: “Driving a car”
operateVehicle(myTruck); // 出力: “Loading a truck”
“`

この例では、operateVehicle関数は Car または Truck のユニオン型である Vehicle を受け取ります。関数内で vehicle instanceof Car というチェックを行うことで、TypeScriptは if ブロック内で vehicle の型を Car にナローイングします。これにより、vehicle.drive() の呼び出しが型安全になります。else ブロックでは、vehicleCar でないことから、ユニオン型のもう一方である Truck 型にナローイングされ、vehicle.load() が安全に呼び出せます。

このように、instanceofはTypeScriptの型ガード機能と連携することで、実行時のオブジェクトの型に応じた安全なコードパスを記述することを可能にします。

instanceofを使った型ガードの実装

単純なクラスインスタンスの判別

最も基本的な使い方は、特定のクラスのインスタンスかどうかを判別する場合です。

``typescript
class Person {
name: string;
constructor(name: string) { this.name = name; }
greet() { console.log(
Hello, my name is ${this.name}`); }
}

class Animal {
species: string;
constructor(species: string) { this.species = species; }
makeSound() { console.log(“Some generic sound”); }
}

type Entity = Person | Animal;

function handleEntity(entity: Entity) {
if (entity instanceof Person) {
console.log(“Handling a person:”);
entity.greet(); // 安全に Person のメソッドを呼び出し
} else {
console.log(“Handling an animal:”);
entity.makeSound(); // 安全に Animal のメソッドを呼び出し
}
}

const alice = new Person(“Alice”);
const leo = new Animal(“Lion”);

handleEntity(alice);
handleEntity(leo);
“`

この例では、Entityというユニオン型に対してinstanceof Personというチェックを行い、Person型にナローイングされたスコープでgreetメソッドを呼び出しています。

継承関係にあるクラスのインスタンス判別

instanceofはプロトタイプチェーンを辿るため、継承関係にあるクラスでも正しく機能します。

``typescript
class Shape {
color: string;
constructor(color: string) { this.color = color; }
describe(): string { return
Shape with color ${this.color}`; }
}

class Circle extends Shape {
radius: number;
constructor(color: string, radius: number) {
super(color);
this.radius = radius;
}
area(): number { return Math.PI * this.radius * this.radius; }
}

class Square extends Shape {
sideLength: number;
constructor(color: string, sideLength: number) {
super(color);
this.sideLength = sideLength;
}
area(): number { return this.sideLength * this.sideLength; }
}

type Drawable = Shape | Circle | Square;

function processShape(shape: Drawable) {
console.log(shape.describe()); // Shape のプロパティ/メソッドは常にアクセス可能

if (shape instanceof Circle) {
console.log(Circle area: ${shape.area()}); // Circle として area() を呼び出し
} else if (shape instanceof Square) {
console.log(Square area: ${shape.area()}); // Square として area() を呼び出し
} else {
// shape は Shape だが Circle でも Square でもない(この例では純粋な Shape インスタンス)
console.log(“This is a generic shape.”);
// Shape に area() メソッドがなければ呼び出せない
}

// instanceof Shape はここではあまり意味がない(shapeがShapeのユニオン型であるため)
// if (shape instanceof Shape) { … } // 常に true (Circle, Square も Shape のインスタンス)
}

const myCircle = new Circle(“red”, 5);
const mySquare = new Square(“blue”, 10);
const myShape = new Shape(“green”);

processShape(myCircle);
// 出力:
// Shape with color red
// Circle area: 78.5…

processShape(mySquare);
// 出力:
// Shape with color blue
// Square area: 100

processShape(myShape);
// 出力:
// Shape with color green
// This is a generic shape.
“`

この例のように、instanceof CircleshapeCircleのインスタンスである場合にtrueを返します。Squareについても同様です。Shapeクラス自体もチェックできますが、ユニオン型がShapeを含む場合は、その派生クラスのインスタンスもShapeのインスタンスと判定されるため、instanceof ShapeだけではCircleSquareではない純粋なShapeインスタンスを区別することはできません。継承関係を考慮する場合、より具体的な(派生)クラスから順番にチェックしていくのが一般的です。

インターフェースとinstanceof

TypeScriptのインターフェース(interface)は、コンパイル時に型の構造を定義するためのものであり、JavaScriptの実行時には存在しません。したがって、インターフェースに対してinstanceof演算子を使用することはできません。

“`typescript
interface Movable {
move(distance: number): void;
}

class Car implements Movable {
move(distance: number) { console.log(Car moved ${distance} units); }
}

class Bicycle implements Movable {
move(distance: number) { console.log(Bicycle moved ${distance} units); }
}

class Boat {
sail() { console.log(“Boat is sailing”); }
}

type Vehicle2 = Car | Bicycle | Boat;

function operateVehicle2(vehicle: Vehicle2) {
// エラー: ‘Movable’ refers to a type, but is being used as a value here.
// if (vehicle instanceof Movable) { … }

// どうすれば Movable なオブジェクトを判別できるか?
// instanceof はクラスに対してのみ使用可能
if (vehicle instanceof Car) {
vehicle.move(10); // OK
} else if (vehicle instanceof Bicycle) {
vehicle.move(5); // OK
} else {
vehicle.sail(); // OK (Boat)
}
}
“`

インターフェースの実装を判別したい場合は、instanceofではなく、後述するユーザー定義型ガードやプロパティ存在チェックなどの代替手段を使用する必要があります。

抽象クラスとinstanceof

抽象クラスは、それ自体をインスタンス化することはできませんが、派生クラスのための基底クラスとして機能します。JavaScriptの実行時においては、抽象クラスは通常のクラス(コンストラクタ関数)として存在します。したがって、抽象クラスを継承した具象クラスのインスタンスに対して、抽象クラス名を使ってinstanceofチェックを行うことは可能です。

“`typescript
abstract class Command {
abstract execute(): void;
}

class SaveCommand extends Command {
execute() { console.log(“Saving…”); }
}

class LoadCommand extends Command {
execute() { console.log(“Loading…”); }
}

type AppCommand = SaveCommand | LoadCommand | null;

function processCommand(command: AppCommand) {
if (command instanceof Command) {
// command は SaveCommand または LoadCommand のいずれか
console.log(“Processing a command…”);
command.execute(); // execute() メソッドは抽象クラスで定義されているため安全
} else {
console.log(“No command to process.”);
}
}

const saveCmd = new SaveCommand();
const loadCmd = new LoadCommand();
const noCmd = null;

processCommand(saveCmd); // 出力: Processing a command… \n Saving…
processCommand(loadCmd); // 出力: Processing a command… \n Loading…
processCommand(noCmd); // 出力: No command to process.
“`

この例では、SaveCommandLoadCommandはどちらも抽象クラスCommandを継承しています。instanceof Commandは、SaveCommandLoadCommandのインスタンスに対してtrueを返します。これにより、processCommand関数内で、渡されたオブジェクトが何らかのCommandの派生クラスのインスタンスである場合に共通のロジック(この場合はexecute()の呼び出し)を実行できます。

より複雑なシナリオでのinstanceof

ユニオン型とinstanceof

前述の例でも示したように、instanceofはユニオン型のメンバーが特定のクラスインスタンスであるかを判別するのに非常に有効です。

“`typescript
type Result = { success: true; data: any } | { success: false; error: Error };

function handleResult(result: Result) {
// Error は JavaScript の組み込みクラス
if (result.success) {
// result の型は { success: true; data: any }
console.log(“Success:”, result.data);
} else {
// result の型は { success: false; error: Error }
// ここで result.error が Error インスタンスであることを確認
if (result.error instanceof Error) {
console.error(“Failed with Error object:”, result.error.message);
} else {
console.error(“Failed with unknown error object:”, result.error);
}
}
}

handleResult({ success: true, data: { id: 1, name: “Item” } });
handleResult({ success: false, error: new Error(“Something went wrong”) });
handleResult({ success: false, error: “An error occurred” }); // error プロパティは Error 型だが、実際には string の場合
“`

この例では、Resultというユニオン型は、成功時のオブジェクトと失敗時のオブジェクトです。失敗時のオブジェクトのerrorプロパティはError型と定義されています。しかし、実際の実行時には、何らかの理由でErrorインスタンスではない値(例えば文字列)が割り当てられる可能性もゼロではありません(特にJavaScriptとの連携部分など)。if (result.error instanceof Error)というチェックを行うことで、実行時に実際にそれがErrorインスタンスであるかを確認し、安全にresult.error.messageにアクセスできます。instanceof ErrorはJavaScriptの組み込みクラスに対しても型ガードとして機能します。

判別可能なユニオン型 (Discriminated Unions) とinstanceof

判別可能なユニオン型は、ユニオン型の各メンバーが共通のリテラルプロパティ(タグ)を持つことで、そのタグの値によってメンバーを識別できる強力なパターンです。

“`typescript
interface Action {
type: string;
}

interface AddTodoAction extends Action {
type: ‘ADD_TODO’;
text: string;
}

interface RemoveTodoAction extends Action {
type: ‘REMOVE_TODO’;
id: number;
}

interface UpdateTodoAction extends Action {
type: ‘UPDATE_TODO’;
id: number;
text: string;
}

type TodoAction = AddTodoAction | RemoveTodoAction | UpdateTodoAction;

function handleTodoAction(action: TodoAction) {
// instanceof はここでは使えない (Action, AddTodoAction, RemoveTodoAction, UpdateTodoAction はインターフェースなので実行時に存在しない)

// 判別可能なユニオン型では、タグプロパティの値で判別するのが一般的
switch (action.type) {
case ‘ADD_TODO’:
// action の型は AddTodoAction にナローイング
console.log(Add todo: ${action.text});
break;
case ‘REMOVE_TODO’:
// action の型は RemoveTodoAction にナローイング
console.log(Remove todo with id: ${action.id});
break;
case ‘UPDATE_TODO’:
// action の型は UpdateTodoAction にナローイング
console.log(Update todo with id ${action.id} to: ${action.text});
break;
default:
// ここに到達する場合、TodoAction に含まれない type が渡されたか、網羅性が足りない
const _exhaustiveCheck: never = action; // never 型を利用した網羅性チェック
break;
}
}
“`

この例のように、判別可能なユニオン型では、通常、タグプロパティ(ここではtype)の値によるswitch文やif/else ifチェーンを使用して型判別を行います。これは、instanceofがインターフェースに対して使えないこと、そしてタグプロパティによる判別が意図を明確に伝えるためです。

しかし、もしユニオン型のメンバーがクラスである場合は、instanceofを判別可能なユニオン型と組み合わせて使用することも理論上は可能です。

“`typescript
class NetworkError {
type = ‘NETWORK_ERROR’ as const;
statusCode: number;
constructor(statusCode: number) { this.statusCode = statusCode; }
}

class DatabaseError {
type = ‘DATABASE_ERROR’ as const;
tableName: string;
constructor(tableName: string) { this.tableName = tableName; }
}

class UnknownError extends Error { // 標準の Error クラスを継承
type = ‘UNKNOWN_ERROR’ as const;
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, UnknownError.prototype); // 継承の際のお作法
}
}

type AppError = NetworkError | DatabaseError | UnknownError;

function handleError(error: AppError) {
// タグプロパティでの判別
switch (error.type) {
case ‘NETWORK_ERROR’:
console.error(Network error: Status ${error.statusCode});
break;
case ‘DATABASE_ERROR’:
console.error(Database error on table ${error.tableName});
break;
case ‘UNKNOWN_ERROR’:
console.error(Unknown error: ${error.message}); // UnknownError は Error を継承しているので message がある
// さらに UnknownError であることの確認もできる
if (error instanceof UnknownError) {
console.error((It's an instance of UnknownError class));
}
break;
}

// または instanceof での判別 (クラスの場合)
if (error instanceof NetworkError) {
console.error(Network error (instanceof): Status ${error.statusCode});
} else if (error instanceof DatabaseError) {
console.error(Database error (instanceof) on table ${error.tableName});
} else if (error instanceof UnknownError) {
console.error(Unknown error (instanceof): ${error.message});
// Error クラスとしても判別できる
if (error instanceof Error) {
console.error((Also an instance of Error class));
}
} else {
// ここには到達しないはず (AppError ユニオン型の網羅性)
}
}

handleError(new NetworkError(500));
handleError(new DatabaseError(“users”));
handleError(new UnknownError(“Something unexpected happened”));
“`

この例では、AppErrorユニオン型のメンバーがクラスなので、instanceofで判別できます。ただし、タグプロパティによる判別(error.typeを使う方法)の方が、判別可能なユニオン型の意図をより明確に伝え、TypeScriptのコンパイラもタグプロパティによるナローイングを強力にサポートするため、一般的にはタグプロパティを使う方が推奨されます。instanceofは、クラスの継承関係を判別したい場合や、標準のErrorクラスのような組み込みクラスを判別したい場合に特に有用です。

ジェネリクスとinstanceof

ジェネリクス(Generics)は、型をパラメータとして扱えるようにする機能であり、主にコンパイル時の型チェックに役立ちます。しかし、ジェネリクスによって指定された型パラメータそのものは、実行時には通常利用できません(型消去されるため)。したがって、ジェネリクス型パラメータに対してinstanceofチェックを直接行うことはできません

“`typescript
class Box {
private value: T;
constructor(value: T) { this.value = value; }
getValue(): T { return this.value; }
}

function processBoxValue(box: Box) {
const value = box.getValue();

// エラー: ‘U’ refers to a type, but is being used as a value here.
// if (value instanceof U) { … }

// どうすれば value の実行時型を判別できるか?
// instanceof を使うなら、具体的なクラス名を指定する必要がある
if (value instanceof HTMLElement) { // 例: value が HTMLElement かどうか
console.log(“Box contains an HTML element.”);
} else if (typeof value === ‘string’) {
console.log(“Box contains a string.”);
} else if (value instanceof Date) { // 例: value が Date オブジェクトかどうか
console.log(“Box contains a Date object.”);
} else {
console.log(“Box contains something else.”);
}
}

const elementBox = new Box(document.createElement(‘div’));
const stringBox = new Box(“hello”);
const dateBox = new Box(new Date());

processBoxValue(elementBox);
processBoxValue(stringBox);
processBoxValue(dateBox);
processBoxValue(new Box(123));
“`

ジェネリックな型パラメータUはコンパイル時にのみ存在するため、instanceof Uのようなコードは実行できません。ジェネリック型を持つオブジェクトの中身の型を実行時に判別したい場合は、その中身の具体的なクラスプリミティブ型に対してinstanceoftypeof、あるいはユーザー定義型ガードを使用する必要があります。

もし、ジェネリッククラスのコンストラクタ自体を引数として受け取ることができるなら、そのコンストラクタを使ってinstanceofチェックを行うことは可能です。

“`typescript
function isInstanceOf(obj: any, constructor: new (…args: any[]) => T): obj is T {
return obj instanceof constructor;
}

class Product {
name: string;
constructor(name: string) { this.name = name; }
}

class Order {
id: number;
constructor(id: number) { this.id = id; }
}

type Item = Product | Order | string;

function processItem(item: Item) {
// instanceof Product / instanceof Order は直接使えるが、
// それを関数にしたい場合など

if (isInstanceOf(item, Product)) {
    console.log("It's a product:", item.name); // item は Product 型にナローイング
} else if (isInstanceOf(item, Order)) {
    console.log("It's an order:", item.id);   // item は Order 型にナローイング
} else {
    console.log("It's something else:", item); // item は string 型にナローイング
}

}

processItem(new Product(“Laptop”));
processItem(new Order(101));
processItem(“text data”);
“`

この例では、isInstanceOfというジェネリックなユーザー定義型ガードを作成し、チェックしたいオブジェクトとコンストラクタ関数を引数として受け取っています。new (...args: any[]) => Tという型は、引数を取ってT型のインスタンスを返すコンストラクタ関数の型を表します。この関数内でobj instanceof constructorというチェックを行うことで、実行時に型判別を行い、TypeScriptコンパイラにobjT型であることを伝えています。これは、ジェネリクスとinstanceofを組み合わせてより柔軟な型判別関数を作る一例です。

クラスファクトリ関数とinstanceof

デザインパターンのファクトリメソッドや抽象ファクトリパターンなど、クラスのインスタンス生成をカプセル化するためにファクトリ関数を使用することがあります。ファクトリ関数から返されたオブジェクトに対しても、そのオブジェクトが特定のクラスのインスタンスであれば、当然instanceofチェックは有効です。

“`typescript
class Warrior {
attack() { console.log(“Warrior attacks!”); }
}

class Mage {
castSpell() { console.log(“Mage casts a spell!”); }
}

type Character = Warrior | Mage;

function createCharacter(type: ‘warrior’ | ‘mage’): Character {
if (type === ‘warrior’) {
return new Warrior();
} else {
return new Mage();
}
}

const char1 = createCharacter(‘warrior’);
const char2 = createCharacter(‘mage’);

if (char1 instanceof Warrior) {
char1.attack(); // OK
}

if (char2 instanceof Mage) {
char2.castSpell(); // OK
}

// ユニオン型の場合の判別
function playCharacter(character: Character) {
if (character instanceof Warrior) {
character.attack();
} else if (character instanceof Mage) {
character.castSpell();
}
}

playCharacter(char1);
playCharacter(char2);
“`

ファクトリ関数が返すオブジェクトは、定義上はCharacterというユニオン型ですが、実行時にはWarriorまたはMageの具体的なインスタンスになります。したがって、そのインスタンスに対してinstanceof Warriorinstanceof Mageというチェックを行い、型を絞り込むことができます。

instanceofの落とし穴と限界

instanceofは強力なツールですが、JavaScriptの実行時の特性に依存しているため、いくつかの落とし穴や限界があります。これらを理解しておくことは、安全な型判別を行う上で非常に重要です。

異なるrealm (iframe, Worker) で生成されたオブジェクト

JavaScriptには「realm」または「global environment」という概念があります。これは、グローバルオブジェクト(windowglobalThis)、組み込みオブジェクトのコンストラクタ(Array, Object, Dateなど)、およびその他のグローバルな定義が存在する実行コンテキストを指します。ブラウザ環境では、異なるオリジンのiframeやWeb Workerは、それぞれ独自のrealmを持ちます。Node.jsでは、vmモジュールを使って新しいrealmを作成できます。

問題は、同じ名前のクラスやコンストラクタであっても、異なるrealmで生成されたものは、厳密等価(===)ではない別のコンストラクタオブジェクトと見なされるということです。instanceof演算子は、オブジェクトのプロトタイプチェーンにあるプロトタイプが、右辺に指定されたコンストラクタのprototypeプロパティと厳密等価であるかをチェックします。

“`html




“`

この例では、メインフレームとiframeの両方でMyClassという名前のクラスを定義しています。メインフレームで生成したobjInMainをメインフレームのMyClassでチェックするとtrueになりますが、iframeで生成したobjInIframeをメインフレームのMyClassでチェックするとfalseになります。これは、objInIframeのプロトタイプチェーンに含まれるiframe.MyClass.prototypeが、メインフレームのwindow.MyClass.prototypeとは異なるオブジェクトだからです。

このrealmの境界を越えたinstanceofの問題は、組み込みオブジェクトでも発生します。例えば、iframe内で生成された配列は、メインフレームのArrayコンストラクタに対するinstanceofチェックでfalseを返すことがあります。

“`javascript
// Main frame context
const mainArray = [1, 2, 3];
console.log(mainArray instanceof Array); // true

// Assuming iframe exists and has created an array:
// const iframeArray = iframe.contentWindow.getIframeArray();
// console.log(iframeArray instanceof Array); // false (potentially)
“`

これは、異なるrealm間でオブジェクトをやり取りする場合(例: postMessageでオブジェクトを渡す、DOM要素を渡す)、instanceofによる型判別が信頼できないことを意味します。

この問題を回避するためには、以下の方法が考えられます。

  • 構造による判別: オブジェクトが持つ特定のプロパティやメソッドの存在で判別する。'propertyName' in objやユーザー定義型ガードが有効です。
  • タグ付け: オブジェクトに特定の識別子となるプロパティ(例: obj.__type = 'MyClass')を付与し、そのプロパティの値で判別する。
  • コンストラクタ名のチェック (非推奨): obj.constructor.name === 'MyClass' のようにコンストラクタ名を文字列で比較する方法もありますが、これはコードの難読化やMinificationによってクラス名が変わる可能性があり、信頼性が低いです。
  • Symbol.toStringTag: 一部の組み込みオブジェクトはSymbol.toStringTagプロパティを持っており、Object.prototype.toString.call(obj)でそのオブジェクトの種類を示す文字列を取得できます(例: [object Array], [object Date], [object Object], [object Arguments]など)。これを型判別に利用することは、組み込みオブジェクトに対しては比較的信頼性の高い方法です。

Symbol.hasInstance メソッドの利用

ES6では、instanceof演算子の挙動をカスタマイズするために、Symbol.hasInstanceというWell-known Symbolが導入されました。クラスやコンストラクタ関数がこの[Symbol.hasInstance]メソッドを実装している場合、obj instanceof Constructorという式は、内部的にConstructor[Symbol.hasInstance](obj)の呼び出しに変換されます。

“`javascript
class MyChecker {
static Symbol.hasInstance {
// ここで独自の判別ロジックを記述
// 例えば、特定のプロパティを持っているか、特定のメソッドがあるかなど
return typeof obj === ‘object’ && obj !== null && ‘myProperty’ in obj;
}
}

const obj1 = { myProperty: 123 };
const obj2 = { anotherProperty: 456 };

console.log(obj1 instanceof MyChecker); // true (Symbol.hasInstance が true を返すため)
console.log(obj2 instanceof MyChecker); // false (Symbol.hasInstance が false を返すため)
console.log({} instanceof MyChecker); // false
console.log(null instanceof MyChecker); // false
console.log(undefined instanceof MyChecker); // false
console.log(“hello” instanceof MyChecker); // false
“`

Symbol.hasInstanceを使用すると、クラスのインスタンスであるかどうかの判定だけでなく、オブジェクトの構造やその他の条件に基づいてinstanceofの挙動をカスタマイズできます。これは強力な機能ですが、同時に注意が必要です。予期しないカスタム[Symbol.hasInstance]を持つクラスに対してinstanceofを使用すると、通常のプロトタイプチェーンに基づくチェックとは異なる結果になる可能性があります。TypeScriptコンパイラは、デフォルトではこのカスタム挙動を考慮せず、プロトタイプチェーンに基づいたナローイングを推論します。カスタム[Symbol.hasInstance]を使用する場合は、型アノテーションやユーザー定義型ガードを適切に使用して、コンパイラに正しい型情報を伝える必要があります。

プリミティブ型に対するinstanceof

既に述べたように、プリミティブ型(string, number, boolean, symbol, undefined, bigint)はオブジェクトではないため、プロトタイプチェーンを持ちません。したがって、プリミティブ値に対してinstanceofを使用すると、必ずfalseが返されます。

“`javascript
const str = “hello”;
const num = 123;
const bool = true;
const sym = Symbol(“id”);
const undef = undefined;
const bigInt = 10n;

console.log(str instanceof String); // false
console.log(num instanceof Number); // false
console.log(bool instanceof Boolean); // false
console.log(sym instanceof Symbol); // false
console.log(undef instanceof Object); // false
console.log(bigInt instanceof BigInt); // false

// オブジェクトラッパーの場合は true
console.log(new String(“hello”) instanceof String); // true
console.log(new Number(123) instanceof Number); // true
“`

プリミティブ型の型判別には、typeof演算子を使用するのが適切かつ推奨される方法です。

null / undefined に対するinstanceof

検査対象のオブジェクトがnullまたはundefinedの場合、instanceofはプロトタイプチェーンを辿ることができないため、必ずfalseを返します。

“`javascript
const obj: any = null;
const anotherObj: any = undefined;
const emptyObj = {};

console.log(obj instanceof Object); // false
console.log(anotherObj instanceof Object); // false
console.log(emptyObj instanceof Object); // true

// typeof null は “object” を返すという歴史的なバグがあるが、
// instanceof null / undefined は TypeError
// console.log({} instanceof null); // TypeError
// console.log({} instanceof undefined); // TypeError
“`

ユニオン型にnullundefinedが含まれる可能性がある場合は、instanceofチェックの前にこれらの値を別途チェックする必要があります。

“`typescript
function processNullableObject(obj: object | null | undefined) {
if (obj === null || obj === undefined) {
console.log(“Input is null or undefined”);
return;
}

// ここから下では obj は object 型にナローイングされている

if (obj instanceof Date) {
console.log(“It’s a Date object:”, obj.toISOString());
} else {
console.log(“It’s some other object:”, obj);
}
}

processNullableObject(new Date());
processNullableObject({});
processNullableObject(null);
processNullableObject(undefined);
“`

if (obj === null || obj === undefined) というチェックは、TypeScriptによってobjobject型であることを保証する型ガードとして機能します。

プロトタイプチェーンの改変による影響

JavaScriptでは、実行時にオブジェクトのプロトタイプチェーンを変更することが可能です(例: Object.setPrototypeOf(), obj.__proto__)。プロトタイプチェーンが変更されると、instanceofの結果もそれに合わせて変化します。

“`javascript
class Base {}
class Derived extends Base {}

const obj = new Derived();
console.log(obj instanceof Derived); // true
console.log(obj instanceof Base); // true

// 実行時にプロトタイプを変更
Object.setPrototypeOf(obj, Base.prototype);

console.log(obj instanceof Derived); // false (Derived.prototype がプロトタイプチェーンから外れた)
console.log(obj instanceof Base); // true (Base.prototype はまだチェーンにある)

Object.setPrototypeOf(obj, Object.prototype);

console.log(obj instanceof Derived); // false
console.log(obj instanceof Base); // false
console.log(obj instanceof Object); // true
“`

プロトタイプチェーンの動的な改変は一般的なパターンではありませんが、ライブラリや特定のフレームワークによっては行われる可能性があります。このような状況下では、instanceofの信頼性が低下する可能性があることを考慮する必要があります。

WebpackやBabelなどのトランスパイル環境での注意点

WebpackやBabelなどのJavaScriptトランスパイラやバンドラは、ES6以降のクラス構文をES5などの古いJavaScriptコードに変換します。この変換の過程で、クラスの継承やコンストラクタの挙動が、元のコードとは微妙に異なる実装になることがあります。

特に、異なるモジュールバンドルやチャンクで生成されたクラスインスタンスに対してinstanceofを使用する場合、異なる変換方法や異なるrealm(WebpackのRuntimeなど)が原因で、予期しない結果になる可能性がゼロではありません。これは、上記のrealmを跨いだ問題と同様の原因(コンストラクタオブジェクトの非厳密等価)によるものです。

この問題は現代の主要なトランスパイラやバンドラでは軽減されていますが、古い設定や特殊な構成を使用している場合には発生しうるため、注意が必要です。可能な限り、同じコンパイル/バンドルプロセスで生成されたクラスインスタンスに対してinstanceofを使用するか、代替の型判別方法を検討するのが安全です。

JavaScriptの組み込みオブジェクトに対するinstanceof

Array, Date, RegExp, ErrorなどのJavaScriptの組み込みオブジェクトに対してもinstanceofを使用できます。TypeScriptはこれを型ガードとして認識します。

“`typescript
function processValue(value: any) {
if (value instanceof Array) {
console.log(“It’s an array:”, value.length); // value は any[] にナローイング
} else if (value instanceof Date) {
console.log(“It’s a date:”, value.toISOString()); // value は Date にナローイング
} else if (value instanceof RegExp) {
console.log(“It’s a regex:”, value.source); // value は RegExp にナローイング
} else if (value instanceof Error) {
console.error(“It’s an error:”, value.message); // value は Error にナローイング
} else {
console.log(“It’s something else:”, value);
}
}

processValue([1, 2, 3]);
processValue(new Date());
processValue(/abc/);
processValue(new Error(“Oops”));
processValue(“hello”);
processValue({});
“`

これは非常に便利ですが、上記「異なるrealm」の問題により、異なるiframeやWorkerで生成された組み込みオブジェクトに対しては信頼性が低下する可能性があります。また、一部の環境や特殊なケースでは、オブジェクトのプロトタイプチェーンが変更されている可能性も考慮に入れる必要があります。

組み込みオブジェクトの型判別には、より信頼性の高い方法としてObject.prototype.toString.call()Symbol.toStringTagを組み合わせる方法があります。

“`javascript
function processValueSafe(value: any) {
const tag = Object.prototype.toString.call(value);

if (tag === '[object Array]') {
    console.log("It's an array (safe):", value.length); // TypeScript はここでは自動ナローイングしない
    const arr = value as any[]; // 型アサーションが必要になる場合がある
} else if (tag === '[object Date]') {
    console.log("It's a date (safe):", value.toISOString());
    const date = value as Date;
}
// 他のタグ ...
else {
    console.log("It's something else (safe):", value);
}

}
“`

このObject.prototype.toString.call()を使用する方法は、realmを跨いでも一貫した結果を返す傾向があるため、組み込みオブジェクトの判別においてはより堅牢とされています。ただし、この方法をTypeScriptの型ガードとして機能させるには、ユーザー定義型ガードと組み合わせる必要があります。

“`typescript
function isArraySafe(value: any): value is T[] {
return Object.prototype.toString.call(value) === ‘[object Array]’;
}

function isDateSafe(value: any): value is Date {
return Object.prototype.toString.call(value) === ‘[object Date]’;
}

// 他の組み込み型ガードも同様に定義

function processValueWithSafeGuards(value: any) {
if (isArraySafe(value)) {
console.log(“It’s an array (with guard):”, value.length); // value は any[] にナローイング
} else if (isDateSafe(value)) {
console.log(“It’s a date (with guard):”, value.toISOString()); // value は Date にナローイング
}
// …
}
“`

このように、組み込みオブジェクトに対してinstanceofを使用することは一般的で便利ですが、特に異なる実行コンテキストを扱う可能性のあるコードでは、その限界を理解し、必要に応じてObject.prototype.toString.call()のような代替手段やカスタム型ガードを検討することが重要です。

instanceofの代替となる型判別方法

instanceofがクラスインスタンスの判別に適している一方、他のシナリオでは別の型判別方法がより適切です。ここでは、主な代替手段を紹介します。

typeof演算子

前述の通り、typeofはプリミティブ型の判別に使われます。TypeScriptはこれを型ガードとして認識します。

typescript
function processPrimitive(value: string | number | boolean) {
if (typeof value === 'string') {
console.log("It's a string:", value.toUpperCase()); // value は string にナローイング
} else if (typeof value === 'number') {
console.log("It's a number:", value.toFixed(2)); // value は number にナローイング
} else {
console.log("It's a boolean:", !value); // value は boolean にナローイング
}
}

typeofはプリミティブ型以外にも'object', 'function', 'undefined', 'symbol', 'bigint'といった文字列を返しますが、'object'の場合、オブジェクト、配列、nullなど多くの型に対して返されるため、これだけでは具体的なオブジェクトの種類を判別できません。また、typeof null'object'になるという歴史的な挙動にも注意が必要です。

ユーザー定義型ガード (User-Defined Type Guards)

これは最も柔軟な型判別方法であり、instanceofが使えないインターフェースの判別や、より複雑な条件での型判別に適しています。parameterName is Typeという形式の型述語を戻り値の型アノテーションに持つ関数として定義します。

“`typescript
interface HasName {
name: string;
}

interface HasId {
id: number;
}

// HasName インターフェースを実装しているか判別するユーザー定義型ガード
function isHasName(obj: any): obj is HasName {
// オブジェクトであり、nullではない、そして ‘name’ プロパティが文字列であることをチェック
return typeof obj === ‘object’ && obj !== null && typeof (obj as any).name === ‘string’;
}

// HasId インターフェースを実装しているか判別するユーザー定義型ガード
function isHasId(obj: any): obj is HasId {
// オブジェクトであり、nullではない、そして ‘id’ プロパティが数値であることをチェック
return typeof obj === ‘object’ && obj !== null && typeof (obj as any).id === ‘number’;
}

type ItemInfo = HasName | HasId | string;

function processItemInfo(item: ItemInfo) {
if (typeof item === ‘string’) {
console.log(“It’s a string:”, item);
} else if (isHasName(item)) {
// item は HasName 型にナローイング
console.log(“It has a name:”, item.name);
// isHasId でないことは保証されない (HasName と HasId は両立しうる)
if (isHasId(item)) {
console.log(“… and an id:”, item.id); // item は HasName & HasId 型にナローイング
}
} else if (isHasId(item)) {
// ここに来た場合、item は HasId だが HasName ではない
console.log(“It has an id:”, item.id);
} else {
// この例では到達しないはず (string | HasName | HasId ユニオン型の網羅性)
// 念のため never 型でチェック
const _exhaustiveCheck: never = item;
}
}

processItemInfo(“Just a string”);
processItemInfo({ name: “Item A” });
processItemInfo({ id: 10 });
processItemInfo({ name: “Item B”, id: 20 });
// processItemInfo({ someProperty: true }); // このコードは型エラーになるが、any を渡すとここに来る可能性がある
“`

ユーザー定義型ガードは、オブジェクトが特定の「構造」を持っているかをチェックするのに適しています。これにより、実行時に存在しないインターフェースの実装を模倣的に判別したり、より複雑な条件でオブジェクトを分類したりできます。instanceofがクラスの継承階層やインスタンス生成元に基づいた判別であるのに対し、ユーザー定義型ガードはオブジェクトの現在の形(Shape)に基づいた判別と言えます。

判別可能なユニオン型 (discriminant property)

タグプロパティ(判別子)を使ったユニオン型は、オブジェクトの型を特定の値を持つ共通プロパティで区別する方法です。これは特にメッセージやイベントオブジェクトなど、構造化されたデータを扱う場合に非常に有効です。

“`typescript
interface SuccessResponse {
status: ‘success’;
data: any;
}

interface ErrorResponse {
status: ‘error’;
message: string;
code?: number;
}

interface LoadingResponse {
status: ‘loading’;
}

type ApiResponse = SuccessResponse | ErrorResponse | LoadingResponse;

function handleApiResponse(response: ApiResponse) {
switch (response.status) {
case ‘success’:
// response は SuccessResponse 型
console.log(“Data received:”, response.data);
break;
case ‘error’:
// response は ErrorResponse 型
console.error(“Error:”, response.message, response.code);
break;
case ‘loading’:
// response は LoadingResponse 型
console.log(“Loading…”);
break;
default:
const _exhaustiveCheck: never = response;
break;
}
}
“`

このパターンは、instanceofやユーザー定義型ガードよりも、定義されたユニオン型のメンバーを明示的に判別するのに優れています。コンパイラがタグプロパティによるナローイングを強力にサポートするため、非常に安全かつ表現力の高いコードを書くことができます。

プロパティ存在チェック ('propertyName' in object)

in演算子を使用して、オブジェクトが特定のプロパティを持っているかを確認することも、ユニオン型内のオブジェクトを判別するための一種の型ガードとして機能します。

“`typescript
interface Cat {
meow(): void;
purr: boolean;
}

interface Dog {
bark(): void;
fetch: boolean;
}

type Pet = Cat | Dog;

function makeSound(pet: Pet) {
if (‘meow’ in pet) {
// pet は Cat 型 (meow メソッドを持っていることから)
pet.meow();
} else {
// pet は Dog 型 (Cat でなく、Pet ユニオン型のもう一方だから)
pet.bark();
}
}

function checkTrait(pet: Pet) {
if (‘purr’ in pet) {
// pet は Cat 型 (purr プロパティを持っていることから)
console.log(“It purrs:”, pet.purr);
} else {
// pet は Dog 型 (Cat でなく、Pet ユニオン型のもう一方だから)
console.log(“It fetches:”, pet.fetch);
}
}

// 注意: meow と bark は両方持つオブジェクトも存在しうる場合は、
// ‘meow’ in pet だけでは Cat にナローイングされないかもしれない
// (depends on TS version and complexity)
// 明示的に型ガード関数を使う方が安全な場合が多い
function isCat(pet: any): pet is Cat {
return typeof pet === ‘object’ && pet !== null && typeof (pet as Cat).meow === ‘function’;
}

function makeSoundSafe(pet: Pet) {
if (isCat(pet)) {
pet.meow();
} else {
pet.bark();
}
}
“`

'propertyName' in objectは、そのプロパティがオブジェクト自身またはプロトタイプチェーン上に存在するかをチェックします。これは、特にオブジェクトが特定のメソッドやプロパティを持つことを期待する場合に便利です。ただし、プロパティが存在するからといって、そのオブジェクトが期待する「型」全体の構造を満たしているとは限りません。より厳密なチェックが必要な場合は、ユーザー定義型ガードの中で複数のプロパティチェックを組み合わせたり、プロパティの型までチェックしたりする必要があります。

コンストラクタ名 / プロトタイプ名による判別 (非推奨)

過去には、obj.constructor.nameObject.getPrototypeOf(obj).constructor.nameなどの方法でコンストラクタ名を文字列として取得し、それを比較することで型を判別する手法が使われることもありました。しかし、これはプロトタイプチェーンの改変に脆弱である上、コードの難読化やMinificationによってクラス名が変更される可能性があり、非常に信頼性が低いため、推奨されません

安全な型判別を実現するためのベストプラクティス

TypeScriptで安全かつ効果的な型判別を行うためには、状況に応じて最適な方法を選択し、それぞれの特徴と限界を理解することが重要です。

  1. クラスインスタンス判別にはinstanceofを優先的に使用する:

    • 渡されたオブジェクトが特定のクラス(またはその派生クラス)のインスタンスであることを確認したい場合、instanceofは最も直接的でTypeScriptの型ガード機能も効くため、最初の選択肢となります。
    • 組み込みクラス(Array, Date, Errorなど)のインスタンス判別にも便利です。ただし、realmを跨ぐ可能性がある場合は、その限界を認識しておくか、より堅牢な代替手段を検討します。
  2. インターフェースや複雑な構造の判別にはユーザー定義型ガードを使用する:

    • 実行時に存在しないインターフェースを実装しているかのようにオブジェクトの構造を判別したい場合。
    • 複数のプロパティやそれらの型、あるいはより複雑な条件を満たすかでオブジェクトを分類したい場合。
    • instanceoftypeofでは表現できない独自の型判別ロジックをカプセル化したい場合。
    • Object.prototype.toString.call()のようなメソッドを使った判別ロジックを型ガードとして活用したい場合。
  3. 構造が固定されたユニオン型には判別可能なユニオン型(タグ付きユニオン)を検討する:

    • オブジェクトがある一連の定義済み構造のいずれかに合致することが保証されており、各構造がユニークなタグプロパティを持っている場合。
    • switch文による判別は、網羅性のチェックも行いやすいため、特に多くのバリエーションがある場合に有効です。
  4. プロパティ存在チェック ('propertyName' in object) は補助的に使用する:

    • 特定のプロパティ(特にメソッド)の存在が、そのオブジェクトが特定のインターフェースや構造を持っていることの強い手がかりとなる場合に使用できます。
    • ただし、プロパティの存在だけでは完全な型の保証にはならないため、慎重に使用するか、ユーザー定義型ガードの中で組み合わせて使用します。
  5. 型ナローイング (Type Narrowing) を理解し活用する:

    • 型ガードは、特定のコードブロック内で変数の型をより具体的なものに絞り込む(ナローイング)ための手段です。型ガードが成功した後のスコープでは、TypeScriptが提供する具体的な型情報を最大限に活用し、安全にプロパティやメソッドにアクセスします。
  6. 実行時の型判別は「信頼できない外部からのデータ」に対して特に重要であると認識する:

    • APIレスポンス、ファイルの内容、ユーザー入力、ローカルストレージのデータなど、アプリケーションの外部から来るデータは、定義された型通りの構造を持っているとは限りません。これらのデータを扱う際には、必ず実行時の型判別(バリデーションを含む)を行い、安全に処理を進めるようにします。
  7. デバッグツールを活用する:

    • 実行時にオブジェクトの実際の構造やプロトタイプチェーンを確認したい場合は、ブラウザの開発者ツールやNode.jsのデバッガーを活用します。これにより、instanceofや他の型判別が期待通りに機能しているかを確認できます。

実践的な例

イベントハンドラの型判別

ブラウザ環境では、様々な種類のイベントが発生します。イベントハンドラはしばしばEvent型の引数を受け取りますが、実際のイベントオブジェクトはMouseEvent, KeyboardEvent, TouchEventなど、より具体的な型を持っています。

``typescript
function handleEvent(event: Event) {
console.log(
Event type: ${event.type}`);

// マウスイベントかどうかを判別
if (event instanceof MouseEvent) {
console.log(Mouse coordinates: (${event.clientX}, ${event.clientY}));
}

// キーボードイベントかどうかを判別
if (event instanceof KeyboardEvent) {
console.log(Key pressed: ${event.key});
}

// タッチイベントかどうかを判別
if (event instanceof TouchEvent) {
console.log(Number of touches: ${event.touches.length});
}

// 組み込みイベントクラスは instanceof で安全に判別できる
}

// 例としてイベントをシミュレート
handleEvent(new MouseEvent(‘click’, { clientX: 100, clientY: 200 }));
handleEvent(new KeyboardEvent(‘keydown’, { key: ‘Enter’ }));
handleEvent(new TouchEvent(‘touchstart’, { touches: [new Touch(), new Touch()] } as any)); // Event コンストラクタは引数が複雑なので any を使用
handleEvent(new Event(‘change’));
“`

ブラウザの組み込みイベントクラスはクラスなので、instanceofを使って安全に型を判別し、それぞれの型固有のプロパティやメソッドにアクセスできます。

APIレスポンスの型判別

APIから取得したJSONデータは、そのままでは特定のTypeScriptの型を持つオブジェクトとはみなされません。実行時にその構造を確認し、期待する型に合致するかどうかを判別する必要があります。APIレスポンスが常にクラスのインスタンスとして返されるわけではないため、この場合はinstanceofよりもユーザー定義型ガードや判別可能なユニオン型が適していることが多いです。

しかし、もしAPIが特定の「エラーオブジェクト」や「成功オブジェクト」をクラスのインスタンスとして返すように設計されている(またはクライアント側で変換する)場合は、instanceofも使用できます。

“`typescript
// サーバーサイドでクラスとしてシリアライズ/デシリアライズされる、あるいはクライアントサイドで変換されると仮定
class ApiSuccess {
constructor(public data: any) {}
}

class ApiError {
constructor(public message: string, public code: number) {}
}

type ApiResponse = ApiSuccess | ApiError | { status: ‘loading’ }; // 例: 別のステータスも含むユニオン

function processApiResponse(response: ApiResponse) {
// クラスインスタンスの判別
if (response instanceof ApiSuccess) {
console.log(“API Success:”, response.data);
} else if (response instanceof ApiError) {
console.error(“API Error:”, response.message, response.code);
}
// 構造による判別 (クラスでない場合)
else if (typeof response === ‘object’ && response !== null && ‘status’ in response && response.status === ‘loading’) {
console.log(“API Status: Loading”);
}
else {
console.warn(“Unknown API response format:”, response);
}
}

// サンプルデータ (実際のAPIからのデータは JSON.parse されたオブジェクト)
// 実行時にはクラスインスタンスではなく、単なるプレーンオブジェクトになる可能性が高い
const successData = { data: { user: “Alice” } };
const errorData = { message: “Not Found”, code: 404 };
const loadingData = { status: ‘loading’ };

// JSON.parse から来たオブジェクトに対する判別は instanceof では難しい場合が多い
// なぜなら、それらはクラスのインスタンスとして「生成」されたわけではないから
// processApiResponse(successData); // ここで instanceof はおそらく false を返す

// このような場合はユーザー定義型ガードが適している
interface ApiSuccessShape { data: any; }
interface ApiErrorShape { message: string; code: number; }
interface LoadingShape { status: ‘loading’; }

function isApiSuccessShape(obj: any): obj is ApiSuccessShape {
return typeof obj === ‘object’ && obj !== null && ‘data’ in obj;
}

function isApiErrorShape(obj: any): obj is ApiErrorShape {
return typeof obj === ‘object’ && obj !== null && ‘message’ in obj && ‘code’ in obj;
}

function isLoadingShape(obj: any): obj is LoadingShape {
return typeof obj === ‘object’ && obj !== null && (obj as any).status === ‘loading’;
}

function processApiResponseShape(response: any) { // APIレスポンスは unknown などで受ける方が安全
if (isApiSuccessShape(response)) {
console.log(“API Success (Shape):”, response.data);
} else if (isApiErrorShape(response)) {
console.error(“API Error (Shape):”, response.message, response.code);
} else if (isLoadingShape(response)) {
console.log(“API Status: Loading (Shape)”);
}
else {
console.warn(“Unknown API response format (Shape):”, response);
}
}

processApiResponseShape(successData); // OK
processApiResponseShape(errorData); // OK
processApiResponseShape(loadingData); // OK
processApiResponseShape({}); // OK
“`

APIレスポンスのように、実行時にクラスのプロトタイプ情報を持たない可能性があるプレーンなJavaScriptオブジェクトの型判別には、instanceofは通常適していません。オブジェクトの「構造」に基づいて判別するユーザー定義型ガードや、明確なタグを持つ判別可能なユニオン型(サーバーとクライアントで合意された構造)を使用するのが一般的なプラクティスです。

UIコンポーネントのプロパティ型判別

ReactやVueなどのフレームワークで、親コンポーネントから子コンポーネントに渡されるプロパティが複数の型を取りうる場合、子コンポーネント内でそのプロパティの型に応じて処理を変えたいことがあります。

“`typescript
interface TextContent {
type: ‘text’;
value: string;
}

interface ImageContent {
type: ‘image’;
url: string;
alt: string;
}

interface VideoContent {
type: ‘video’;
src: string;
}

// もしクラスとして定義されていたら instanceof も可能
class TextContentClass {
type = ‘text’ as const;
constructor(public value: string) {}
}
class ImageContentClass {
type = ‘image’ as const;
constructor(public url: string, public alt: string) {}
}

type Content = TextContent | ImageContent | VideoContent;
type ContentClass = TextContentClass | ImageContentClass; // クラスの場合

function renderContent(content: Content) {
// 判別可能なユニオン型で判別
switch (content.type) {
case ‘text’:
console.log(Rendering Text: ${content.value}); // content は TextContent
break;
case ‘image’:
console.log(Rendering Image: ${content.url} (${content.alt})); // content は ImageContent
break;
case ‘video’:
console.log(Rendering Video: ${content.src}); // content は VideoContent
break;
}
}

function renderContentClass(content: ContentClass) {
// instanceof で判別 (クラスの場合)
if (content instanceof TextContentClass) {
console.log(Rendering Text (Class): ${content.value});
} else if (content instanceof ImageContentClass) {
console.log(Rendering Image (Class): ${content.url} (${content.alt}));
}
}

// 通常はインターフェースやタグ付きユニオンを使うことが多い
renderContent({ type: ‘text’, value: ‘Hello World’ });
renderContent({ type: ‘image’, url: ‘example.jpg’, alt: ‘Example Image’ });
renderContent({ type: ‘video’, src: ‘example.mp4’ });

// クラスを使う場合はインスタンスを渡す
renderContentClass(new TextContentClass(‘Hello from Class’));
renderContentClass(new ImageContentClass(‘class.png’, ‘Class Image’));
“`

UIコンポーネントのプロパティは、多くの場合、データの受け渡しのためにプレーンなオブジェクトやプリミティブ型として定義されます。このような場面では、インターフェースと判別可能なユニオン型、あるいはプロパティ存在チェックやユーザー定義型ガードが自然な型判別方法となります。instanceofは、プロパティとして明示的にクラスインスタンスを受け渡す設計になっている場合に有効です。

まとめ

この記事では、TypeScriptにおけるinstanceof演算子を用いた型判別について、その仕組み、使い方、そして重要な注意点や代替手段を詳細に解説しました。

instanceofは、JavaScriptのプロトタイプチェーンに基づいた機能であり、あるオブジェクトが特定のクラスのインスタンスであるかを実行時にチェックします。TypeScriptにおいては、このinstanceofチェックが強力な型ガードとして機能し、コンパイラがチェック成功後のスコープで検査対象の変数の型を自動的にナローイングしてくれるという大きなメリットがあります。これにより、クラスの継承関係を含むインスタンスの判別や、JavaScriptの組み込みクラスの判別を型安全に行うことができます。

しかし、instanceofは万能ではありません。異なるJavaScript realmを跨いだオブジェクト、プリミティブ型、null/undefined、あるいはプロトタイプチェーンが動的に改変されたオブジェクトに対しては、予期しない結果を返す可能性があります。また、コンパイル時にのみ存在するインターフェースの判別には使用できません。

安全な型判別を実現するためには、instanceofだけでなく、typeof、ユーザー定義型ガード、判別可能なユニオン型、プロパティ存在チェックなど、TypeScriptが提供する他の型判別手法も理解し、それぞれの特徴と適用範囲に応じて適切に使い分けることが重要です。

  • クラスインスタンスや組み込みクラスの判別にはinstanceof
  • インターフェースや複雑な構造の判別、あるいはカスタムロジックにはユーザー定義型ガード
  • 固定された構造のユニオン型には判別可能なユニオン型(タグ付きユニオン)
  • プリミティブ型の判別にはtypeof
  • プロパティの存在をシンプルに確認するには'propertyName' in object

これらの型判別手法を組み合わせることで、実行時に様々な構造を持つ可能性のあるデータを安全に処理し、TypeScriptの静的型付けの恩恵を最大限に活かすことができます。

型安全なコードを書くことは、特に大規模なアプリケーション開発において、バグの削減、コードの可読性の向上、保守性の向上に繋がります。instanceofを含むこれらの型判別メカニズムを深く理解し、日々のTypeScript開発に活かしていきましょう。

コメントする

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

上部へスクロール