JavaScript における型判定手法について

動的型付けの言語を扱っていれば、任意の型の値が入ってくる場合があるだろうし、その型を特定したいという需要がしばしば存在する。例えば Python であれば次のようにして型を判定することができる。

```python
# 判定する関数
def is_str(value): bool:
	return type(value) is str

is_str("文字列") # -> True
is_str(42) # -> False
```

割と簡単に文字列かどうかを判定する関数を実装できるであろう。

しかし、 JavaScript においては、型判定の手法は上記ほどに一筋縄ではないようだ。
本記事では特定の型であることを判定する手法を幾つか紹介する。
また、こうした型を判定する手法を TypeScript で実装するには工夫が必要だと考えられるので、 TypeScript で型判定を実装するにあたって意識した方がいい内容を合わせて説明する。
但し、ここで記載する方法は完璧な方法だとは思ってないので、他にも方法があったら教えてください。

## 型の判定方法: 文字列を例として

まず型判定の手法について、具体的にどういった手法があるか紹介する。
様々な型に対する型判定関数を用意するつもりでいるが、ここでは文字列型 `string` の場合を例として説明する。

```javascript
const isString = (value) => {
	return /* ここに何を記述するか... */;
};
```

### 手法1: `typeof` を使った方法

1番典型的なものは、次のように [`typeof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof) という演算子を使って判定する方法である。

```javascript
const isString = (value) => {
	return typeof value === "string";
};

isString("文字列") // -> true
isString([1,2,3]) // -> false
```

これは `typeof` が値の種類によって `"string"` とか `"number"` とか `"object"` などと文字列で型を返すことを利用しており、基本的にはこれで問題はない。

しかし、少し値をいじってしまうと `typeof` は正しく判定しなくなる。
例えば次のように `typeof` でうまく動作しなくなるケースがある。

```javascript
const a = "text";
const b = Object(a);
const c = new String(a);
typeof a // -> "string"
typeof b // -> "object"
typeof c // -> "object"
```

これは所謂プリミティブ型とラッパー型の違いによるものである。

JavaScript では `string`, `number`, `boolean`, `bigint`, `symbol`, `null`, `undefined` は[プリミティブ型](https://developer.mozilla.org/en-US/docs/Glossary/Primitive)とされている。これらはオブジェクトではないからプロパティを持たず、またその値自身は書き換えできない。

一方で、文字列型の値 `s` に対して `s.length` とか `s.splice` とかいうプロパティやメソッドが存在するが、これらは `String` クラスにおけるプロパティやメソッドである。プリミティブな `string` に対して `.length` や `.splice` などを呼んだときには、 `String` クラスのオブジェクトに変換されて呼び出されている。こうした `string` に対する `String` クラスのオブジェクトのことをラッパーオブジェクトと呼ぶ。

そして、 `typeof` では文字列のプリミティブ型は `"string"` を返し、ラッパーオブジェクトは `"object"` を返すようになっている。

上記の例では、 `a` はプリミティブ型であるが `b`, `c` がラッパーオブジェクトに変換されているために、異なる結果を返しているようだ。

「私は絶対プリミティブ型しか扱わない!」というのであればこの方法で問題ないが、任意の値が入ってき得るケースにおいて `String` のラッパーオブジェクトが入ってきて、文字列ではないと判定されるのは困るといったケースでは `typeof` を使った判定はあまり好ましくない。

#### 参考: `typeof` の挙動

`typeof` の挙動はこのような感じである。

* `string`, `number`, `boolean`, `bigint`, `symbol` について
	* プリミティブ型そのものなら `"string"`, `"number"` などを返す
	* ラッパーオブジェクトになると `"object"` を返す
* `undefined` は `"undefined"` を返す (ラッパーオブジェクトは存在しない?)
* `null` は常に `"object"` を返す (ヌルポインタに相当するから)
* 関数型は `typeof` で `"function"` を返す
	* しかもプリミティブ型とは違い、 `Object( )` や `new Function( )` を通しても常に `"function"` を返す
* その他の非プリミティブ型は常に `"object"` を返す

つまり、非プリミティブ型に関してはほとんど役に立たない。

### 手法2: `constructor` を使った方法

値の生成元クラスを得るプロパティである [`.constructor`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor) を使う方法である。これは最初に示した Python の方法に近い方法である。

```javascript
const isString = (value) => {
	// 先に null や undefined の可能性を排除する
	if (value==null) return false;
	return value.constructor === String;
};
```

この方法であればプリミティブではない型についても、それぞれのクラスを知ることができて良さそうである。

しかし、そもそも [`.constructor` プロパティの値は簡単に書き換えられてしまう](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor#changing_the_constructor_of_a_constructor_functions_prototype)ため、この方法だと書き換えられてしまった値については正確に判定することはできない。

`String` から作ったサブクラスの値も `.constructor` は `String` ではなくなってしまうので、判定できない。

### 手法3: `Object` の `toString` を使った方法

[`Object` クラスの値に含まれる `toString` メソッド](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString)の値を使って判定する方法である。これであれば `.constructor` が書き換えられても、サブクラスであっても安全に判定できる。

```javascript
const isString = (value) => {
	return Object.prototype.toString.call(value) === "[object String]";
};

isString("文字列") // -> true

// constructor が偽装されても大丈夫
const a = "文字列";
a.constructor = Object;
isString(a) // -> true

// サブクラスであっても大丈夫
class SubString extends String {}
const b = new SubString("文字列");
isString(b) // -> true
```

`Object.prototype.toString.call` とは

1. `Object.prototype` は `Object` 型のプロトタイプオブジェクトを返す。
	* プロトタイプとは `Object` 型や、 `Object` を継承した全ての型の値の「原型」となる値である。つまり、プロトタイプに属性を追加すれば、これら全ての値に反映される。
2. `Object.prototype.toString` とは `Object` 型についてくる、文字列表現を返す `toString` メソッドのことである。
3. `.call()` とは、 `.toString` 関数の `this` を渡した値に「すり替えた」上で `toString` を実行するということである。通常の `Object` 型の値 `a` に対して `a.toString()` を実行すると、 `toString` 関数内での変数 `this` は `a` となり、 `a` について文字列表現をするが、これを `call` に渡した値にすり替えることにより、その値についての文字列表現を返す。

`Object` の `toString` 関数では、内部的に用意されているクラスに関する情報をもとに `[object String]` や `[object Array]` などのクラス名を示した文字列を返す。サブクラスを作っても継承元の標準で用意されたクラスの結果を返す (これを変える方法は以下で説明する)。

これを聞くと `.constructor` よりは安全そうに見えるが、これもやはり書き換えることはできる。次のようにオブジェクトの [`Symbol.toStringTag` プロパティ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag)を書き換えることで、内部情報よりも優先されてユーザーの指定した値が表示されるようになる。

```javascript
let a = {};
Object.prototype.toString.call(a) // -> [object Object]
a[Symbol.toSymbolTag] = "FakeTag";
Object.prototype.toString.call(a) // -> [object FakeTag]
```

つまり、 `Object` と `toString` を使った方法でも書き換えによって正常に判定できなくなることがあることには注意する。

逆に言えば、自作クラスを作る際には `Symbol.toStringTag` のゲッターを生やしておけば、標準クラスのように見せることができるし、型判定で自作クラスを判別することができるようになる。但し、あくまで文字列の等価性での判定なので、同名のクラスが存在すれば区別できなくなってしまうが...

```javascript
class MyClass {
	get [Symbol.toStringTag]() {
		return "MyClass";
	}
}
```

※ ゲッターを生やせば標準クラスのように見せかけることができると説明したが、多くの標準クラスの値では `Symbol.toStringTag` プロパティは設定されてないようなので、 `Symbol.toStringTag` プロパティの存在有無を調べれば標準クラスに紛れた自作クラスをある程度炙り出すことはできそう。

```javascript
const a = new String("text");
a[Symbol.toStringTag] // undefined
```

### 手法4: `instanceof` を使った方法

`typeof` と同じく演算子の [`instanceof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) を使って判定する手法である。

```javascript
const isString = (value) => {
	return value instanceof String;
};
```

これは `typeof` とは逆でプリミティブ型は判定できず、オブジェクトのみを判定することができる。

```javascript
const a = "text";
const b = Object(a);
const c = new String(a);
const d = new Number(42);
a instanceof String // -> false
b instanceof String // -> true
c instanceof String // -> true
d instanceof String // -> false
```

`typeof` がオブジェクトであることが判定できても、オブジェクトの具体的な内容が判定できないことを補うような機能になっている。手法3のように文字列の等価性による判定ではないから、同名のクラスがあっても安全に判定できる。

手法2,手法3では結果を改竄することができる話をしてきたが、こちらも同様に改竄することができてしまう。というのも、この真偽の判定はオブジェクトの [`Symbol.hasInstance` メソッド](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/hasInstance) に依拠しているので、これを書き換えてしまえばいい。

```javascript
// String が全て偽の判定をするように書き換える
Object.defineProperty(String, Symbol.hasInstance, {
	value: (instance) => false
});
const c = new String("text");
c instanceof String // false
```

### 手法のまとめ

ここまで4つの手法を見てきた。

先ほども言ったが、プリミティブ型に限定すれば `typeof` を使った手法1で十分であるが、やはりラッパー型にも対応させたい。
改竄される危険性はあるものの、ラッパーしたものも含むオブジェクトの判定には `instanceof` を使った手法4が最も確実そうである。

つまり、文字列を判定するための関数は次のようになる。

```javascript
const isString = (value) => {
	return typeof value === "string" || value instanceof String;
};
```

また、プリミティブでない他のクラスの判定には `instanceof` を使えば良さそうである。

```javascript
const isURL = (value) => {
	return value instanceof URL;
};
```

## 型の判定方法: 配列の場合

配列はプリミティブ型ではないので、 `value instanceof Array` で調べることはできる。だが、ここでは配列に特有の内容が幾つかあるのでまとめる。

### 配列のための専用の判定方法

配列を判定する方法として標準で `Array.isArray` が用意されている。

```javascript
const isArray = (value) => Array.isArray(value);
```

これは、 `instanecof` が偽装されても動作する。

```javascript
// Array で instanceof が正常に動作しないようにする
Object.defineProperty(Array, Symbol.hasInstance, {
	value: (instance) => false
});
[] instanceof Array // -> false
Array.isArray([]) // -> true
```

### 要素の型まで含めた判定方法

上記の配列の判定方法は、要素の種類については判定していない。つまり型は `any[]` となる。これを要素に含まれる型まで厳密にチェックするのであれば、次のようにする。

```javascript
// string[] かどうかの判定
const isStringArray = (value) => {
	Array.isArray(value) && value.every(isString);
};
```

ただ、これは要素全てを走査することになるから、パフォーマンスが求められる場面ではあまり好ましくないな...

### 配列のようなオブジェクトも含めた判定方法

所謂 `Array` クラス以外にも配列っぽい挙動を示すクラスは幾つか存在する。例えば DOM 要素に対する `childNodes` や `children` とか、 `RegExp` のキャプチャグループが挙げられる。

これらは `Iterator` になっているらしく、これらまで含めてマッチさせたい場合は、 `Symbol.iterator` プロパティの存在を確認する。

```javascript
const isIterator = (value) => {
	if (value==null) return false;
	return typeof value[Symbol.iterator] === "function";
};
```

## 型の判定方法: オブジェクト

配列に対して、文字列のキーに対して値を持った型としてのオブジェクトを見つける需要が存在する場合、次のようにすれば良さそうである。

```javascript
const isObject = (value) => {
	return typeof value === "object" && value !== null && !isIterable(value);
};
```

まず `typeof value === "object"` で任意クラスのオブジェクトと `null` に絞られ、次の `value !== null` で `null` が排除され、最後の `!isIterable(value)` で配列のようなもの (イテレータ) の可能性を排除している。

## 型の判定方法: `null` と `undefined`

`null` や `undefined` は普通に等価演算子で調べれば良さそうである。

```javascript
const isNull = (value) => value === null;
const isUndefined = (value) => value === undefined;
const isNil = (value) => value == null;
```

`isNil` は `null` と `undefined` の両方にマッチする。

## TypeScript における型

JavaScript を使う場面は TypeScript を使っていることも多いので、 TypeScript として型を持った関数として実装したい。

### プリミティブ型について

TypeScript ではプリミティブ型と、そのラッパーオブジェクトに対して、異なる型が用意されている。

| プリミティブ型 | ラッパーオブジェクトの型 |
|:--:|:--:|
| `string` | `String` |
| `number` | `Number` |
| `boolean` | `Boolean` |
| `bigint` | `BigInt` |
| `symbol` | `Symbol` |

[TypeScript の公式ドキュメント](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#the-primitives-string-number-and-boolean)には次のように書かれている。

> The type names `String`, `Number`, and `Boolean` (starting with capital letters) are legal, but refer to some special built-in types that will very rarely appear in your code. Always use `string`, `number`, or `boolean` for types.

なので上記のラッパーオブジェクト向けの型は普通は使用しない方がいい。

しかし、任意のオブジェクトに対して型判定を行い、ラッパーオブジェクトも文字列などとして適切に判定してほしい、と考えている我々はラッパーオブジェクトも扱える型は必要である。 `String` 型の値を `string` 型の変数に代入することはできないのだから、 `String` を使わざるを得ない。

### 型判定関数の戻り値の型

TypeScript では条件分岐や三項演算子において条件式に型が絞り込めるような式が含まれていると、条件式に合わせて分岐先の式の型を決めてくれるようだ。

次の関数を例に説明する。

```typescript
function describe(value: Nullable<string>) {
	// value は string | null | undefined 型である。
	if ( value == null ) {
		// value が null か undefined の場合しか到達しない
		console.log("The given value is not a string.");
	} else {
		// value が string 型の場合しか到達しないため、 value は既にアンラップされている。
		console.log(`The given value is a string whose length is ${value.length}.`);
	}
}
```

関数の引数 `value` は `Nullable<string>` 、つまり `string | null | undefined` という論理和型である。
TypeScript では `value == null` という条件式により `else` 節には `string` の場合しか到達しないことを察知して、 `else` 節内の変数 `value` は明示的にアンラップ操作をしなくても単純な `string` 型だとみなしてくれる。
他の言語における `if let { } else { }` ではアンラップした文字列型を別の変数に置いた上で `if` 節が実行されるのとは違っている。

この条件式を `value == null` から等価な `isNil(value)` に書き換えても動作するようにしたい。そのためには `isNil` 関数の型の指定を工夫する必要がある。

関数 `isNil` を型を明示して定義すると次のようになるだろう。

```typescript
type IsNil = (value:unknown) => boolean; // 関数の型を予め定義している
const isNil: IsNil = (value) => value == null;
```

この定義だと戻り値はブール型であり、 `Nullable<string>` をアンラップできる条件式が作れるか、という情報は含まれていないため、 `describe` 関数の `else` 節の `value` は依然として `Nullable<string>` になってしまい、コンパイルエラーとなる。
正常にコンパイルさせるにはブール値が「引数が `null` であることを示すブール値」であるという情報を与えなければならない。そのために次のように書き換える必要がある。

```typescript
type Nil = null | undefined; // これで Nullable<string> = string | Nil になる
type IsNil = (value: unknown) => value is Nil;
const isNil: IsNil = (value) => value == null;
```

型として `boolean` の代わりに `value is Nil` を与えることで、 `isNil` 関数の結果が `true` であると `value` が `null` 又は `undefined` であると宣言したことになる。

同様にすれば `isString` 関数も次のようになるだろう。

```typescript
type IsString = (value: unknown) => value is String;
const isString = (value) => {
	return typeof value === "string" || value instanceof String;
}
```

### 型変数を使った抽象化

TypeScript を使って型を導入したからには、型変数を使って楽をしたい。

#### 型判定関数

次のように定義する。

```typescript
type Is<T> = (value: unknown) => value is T;
```

すると関数を実装する度に型定義をする必要はなくなる。

```typescript
const isNil: Is<Nil> = ...;
const isString: Is<String> = ...;
```

#### 関数をまとめて生成

型判定関数の実装は似ているところが多いので、まとめて生成させることもできる。

```typescript
// プリミティブ型およびそのラッパーオブジェクト向け
const makeIsPrimitiveOrWrapper = <T,>(typeofVal:string,cls:Function): Is<T> => {
	return (value) => {
		return ( typeof value === typeofVal ) || ( value instanceof cls )
	};
};
// オブジェクト向け
const makeIsObjectType = <T,>(typeofVal:string,cls:Function): Is<T> => {
	return (value) => value instanceof cls;
};

const isString = makeIsPrimitiveOrWrapper<String>("string",String);
const isNumber = makeIsPrimitiveOrWrapper<Number>("number",Number);
const isBoolean = makeIsPrimitiveOrWrapper<Boolean>("boolean",Boolean);
const isBigInt = makeIsPrimitiveOrWrapper<BigInt>("bigint",BigInt);
const isSymbol = makeIsPrimitiveOrWrapper<Symbol>("symbol",Symbol);

const isRegExp = makeIsObjectType<RegExp>(RegExp);
const isDate = makeIsObjectType<Date>(Date);
const isMap = makeIsObjectType<Map>(Map);
// ...etc
```

#### 配列の判定関数

配列であるか判定する関数は TypeScript では次のように記載できる。

```typescript
const isArray: Is<any[]> = (value) => Array.isArray(value);
```

実際に使う場面では `any[]` よりも `string[]` のように具体的な要素の型の情報もあった方がいいから `isTypedArray` は必要で

```typescript
const isTypedArray = <T>(value: unknown, isTypeForItems: Is<T>): value is Is<T[]> {
	if (!isArray(value)) return false;
	// この時点で value is any[]
	return value.every(isTypeForItems);
};

// 例
function describe(value:unknown) {
	if (!isTypedArray(value,isString)) console.log("The given value is not a string[].");
	else console.log(`The given value is string[]: ${value.join(", ")}`);
}
```

`string[]` などのようによく使われる型は、各要素を判定するための `isString` などを与えるのは面倒なので、ある程度用意しておこう。

```typescript
const makeIsTypedArray = <T>(isTypeForItems: Is<T>): Is<T[]> => {
	return (value) => isTypedArray(value, isTypeForItems);
};

const isStringArray: Is<String[]> = makeIsTypedArray(isString);
const isNumberArray: Is<Number[]> = makeIsTypedArray(isNumber);
const isBooleanArray: Is<Boolean[]> = makeIsTypedArray(isBoolean);
```