JavaScript でも、Python の type みたいに詳しく型orオブジェクト名を得る方法を探る

コメントでの指摘に伴い、公開から間もなく内容を大規模改修しました。ご指摘頂いた @think49 さん、ありがとうございました。

タイトル:
~~JavaScript でも、Python の type みたいに本当の型を取得したい~~
-> JavaScript でも、Python の type みたいに詳しく型orオブジェクト名を得る方法を探る

まだまだ知らないことだらけや･･･。

あと、参考にするどころか、結局 9割こちらの記事のパクりになってしまった･･･。

https://qiita.com/south37/items/c8d20a069fcbfe4fce85

先に最終コードが必要なら、[スキップしてこちらの章をどうぞ。](#正確性重視の判定関数)

# Python の `type` では

```py
type(5)
# <class 'int'>

type("hello")
# <class 'str'>

from datetime import timedelta
obj = timedelta()
type(obj)
# <class 'datetime.timedelta'>
```

`type` に任意のオブジェクトを渡すと、しっかりクラス名を出してくれます。

# 一方 JavaScript では･･･

`int` や `str` など、[プリミティブ型](https://developer.mozilla.org/ja/docs/Glossary/Primitive)の判断は `typeof` によって実現できます。

```js
console.log(typeof 5);
// number

console.log(typeof "hello");
// string
```

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/typeof

しかし、プリミティブ型以外は、**`null` も含めて**全部 `object` と返されてしまうという問題があります。でも実際、これはちゃんと JavaScript 規格通りで、不具合でもなんでもありません。

```js
console.log(typeof null);
// object

console.log(typeof []);
// object
```

:::note warn
`typeof null === "object"` となるのは、[歴史的経緯のため](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/typeof#typeof_null)のようです。
:::

「肝心なその Object が、もっと詳しく何型か知りたい！」おそらく、そんな経緯でこの記事にたどり着いたのではないかと思っています。

# 本題に入る前に･･･型の話

JavaScript の世界、どうやら型の概念がメチャややこしいみたいなんです。まとめるだけまとめたけど、正直あってるか不安･･･。



とりあえず要点だけ抜くと、

* `typeof` でわかるヤツ**だけ**が、本当の「型」
  * null は object と出るが、ちゃんと null 型がある
  * function は function って出るけど、型じゃなくて Object の一種
* 型のうち Object 以外をプリミティブ型と言う
* 同じプリミティブでも、状況によって「プリミティブラッパーオブジェクト (つまり Object の一種)」に変身することも
* Object のもっと詳しい名前のことは「標準組み込みオブジェクト名」あるいは「クラス名」

:::note
ちなみに Python では、(`type` の表示によると) int も str も自作クラスも全部 Class となっています。同じ動的型付け言語だけど全然違う！
:::

# 真の姿を確かめるのに使えそうなもの

ここで、自作クラスとして次を用意して検証しました。

```js
class MyAnyObj {
  constructor() {}
}

myAnyObj = new MyAnyObj();
```

## `typeof`

* JavaScript 規格の定義上、本来の「型」を得られる
* :o: どの値を入れてもエラーにならない
* 得られるもの
  * :o: プリミティブ型 (null 除く)
  * :x: プリミティブラッパーオブジェクト (Object と出る)
  * :x: 標準組み込みオブジェクト名 (Object と出る)
  * :x: クラス名 (Object と出る)

```js
typeof undefined;     // 'undefined'
typeof 2;             // 'number'
typeof new Number(2); // 'object'
typeof ["hello"];     // 'object'
typeof myAnyObj;      // 'object'
```

## `Object.prototype.toString.call`

* :o: どの値を入れてもエラーにならない
* :x: プリミティブはラッパーオブジェクトに化ける
* 得られるもの
  * :x: プリミティブ型 (ラッパーに化ける)
  * :o: プリミティブラッパーオブジェクト (ただし本当のプリミティブと混ざる)
  * :o: 標準組み込みオブジェクト名
  * :x: クラス名 ([object Object] と出る)

```js
Object.prototype.toString.call(undefined);     // '[object Undefined]'
Object.prototype.toString.call(2);             // '[object Number]'
Object.prototype.toString.call(new Number(2)); // '[object Number]'
Object.prototype.toString.call([]);            // '[object Array]'
Object.prototype.toString.call(myAnyObj);      // '[object Object]'

'[object Object]'.slice(8, -1);                // 'ObjClassName'
// String の slice メソッドで不要な箇所をカット
```

<details><summary>一応 .toString() と String() も試してみたけど、全然ダメそうだった。</summary>

## `.toString`

```js
undefined.toString();     // SyntaxError
2.toString();             // '2'
new Number(2).toString(); // '2'
["hello"].toString();     // 'hello'
myAnyObj.toString();      // '[object Object]'
```

## String()

```js
String(undefined);     // 'undefined'
String(2);             // '2'
String(new Number(2)); // '2'
String("hello");       // 'hello'
String(myAnyObj);      // '[object Object]'
```

</details>

## `<obj>.constructor.name`

* コンストラクタ関数を取得し、その名前 (=クラス名) を得るという計算
* :x: `undefined` など一部プリミティブではエラーになる
* :x: 一部プリミティブはラッパーオブジェクトに化ける
* :x: `constructor` を書き換えられてしまっていたら、正しい結果にならない
  * つまりユーザの具合で結果を改変できる
  * (でもこれを書き換えてしまうことってあるのか･･･？)
* 得られるもの
  * :x: プリミティブ型 (一部エラー, ラッパーに化ける)
  * :o: プリミティブラッパーオブジェクト (ただし本当のプリミティブと混ざる)
  * :o: 標準組み込みオブジェクト名
  * :o: クラス名

```js
undefined.constructor.name; // TypeError
2.constructor.name;         // SyntaxError
"hello".constructor.name;   // 'String'
// ↑ ラッパーオブジェクトに化けるのとそうでないのとある
[].constructor.name;        // 'Array'
myAnyObj.constructor.name;  // 'MyAnyObj'

// 2 (Number) は一度変数に入れたら動く, ただしラッパーオブジェクト化
num = 2;
num.constructor.name;       // 'Number'
```

`constructor` 書き換えの例:

```js
obj = new Array();
obj.constructor = "hello";
obj.constructor;      // 'hello'
obj.constructor.name; // undefined
```

# 関数にまとめる

## 正確性重視の判定関数

以上を踏まえて、任意のオブジェクトの判定は、このようなコードに落ち着きました。

```js
function typeof_improved(obj) {
  // null だけちょっと例外的なので先に除外
  if(obj === null) {
    return "null";
  }

  // プリミティブの判定
  let obj_type = typeof obj;
  if(obj_type !== "object") {
    return obj_type;
  }

  // 標準組み込みオブジェクトの判定
  obj_type = Object.prototype.toString.call(obj).slice(8, -1);
  if(obj_type !== "Object") {
    return obj_type;
  }

  // クラス名取得
  return obj.constructor.name;
  // 最後は constructor が書き換えられていないことを前提にするしかない
  // ちなみに - {any: "value"} のような、クラスでもないオブジェクトなら、ここで "Object" となる
}
```

(ワンライナーにするには厳しいコード量･･･)

## 簡易版 (ザックリ分かれば良い)

「本当のプリミティブ or プリミティブラッパーオブジェクト･･･は大事じゃない」という条件下で使える、簡易版はこちら。

```js
function typeof_improved(obj) {
  let type = Object.prototype.toString.call(obj).slice(8, -1);
  if(type !== "Object") {
    return type;
  } else {
    return obj.constructor.name;
  }
}
```

三項演算子で圧縮すると、

```js
let obj_type = Object.prototype.toString.call(obj).slice(8, -1);
obj_type = obj_type !== "object" ? obj_type : obj.constructor.name;
```

## そもそも、「この型か否か」さえ分かれば良いなら

`instanceof` でヨシ。

```js
if(obj instanceof Array) {
  //
}
```

※ ただし、`prototype` が書き換えられていなければの話

詳しくは、[上で示した記事の「instanceof演算子を用いた判定」の章にて](https://qiita.com/south37/items/c8d20a069fcbfe4fce85#instanceof%E6%BC%94%E7%AE%97%E5%AD%90%E3%82%92%E7%94%A8%E3%81%84%E3%81%9F%E5%88%A4%E5%AE%9A) (丸投げ)

# 旧記事内容

<details><summary>正確でなかった、旧内容はこちら</summary>

# 他の方法で型名を得る

`null` と `undefined` を除いて、`.constructor.name` を呼び出すことによっても型名を知ることができます。この方法だと、オブジェクトの**クラス名を知ることができる点**で優位です。

## 1. `null` か判断する

シンプルに比較演算子で OK。例外的な動作をするのは先に弾いておきます。

```js
obj === null
```

## 2. プリミティブ型か判断する

もしプリミティブ型なら、そのまま `typeof` の結果を返せば OK。

```js
typeof obj !== "object"
```

## 3. クラス名を得る

残るは `Object` のみなので、安心してクラス名取得を行えます。

```js
obj.constructor.name
```

# 以上をまとめて

コードに起こすとこんな感じ。

```js
function true_obj_name(obj) {
  if(obj === null) {
    return "null";
  }

  if(typeof obj !== "object") {
    return typeof obj;
  } else {
    return obj.constructor.name;
  }
}
```

3項演算子で圧縮するとこんな感じ。

```js
function true_obj_name(obj) {
  return obj === null ? "null" : typeof obj !== "object" ? typeof obj : obj.constructor.name;
}
```

ワンライナーで書いてしまうとこんな感じ。

```js
const obj = new AnyObj();
const obj_type = obj === null ? "null" : typeof obj !== "object" ? typeof obj : obj.constructor.name;
```

# 別解

プリミティブ型については、`undefined` を除いて同じように `.constructor.name` を呼び出せます。なので、次も同様の結果が得られます。

```js
function true_obj_name(obj) {
  if(obj === null) {
    return "null";
  } else if(typeof obj === "undefined") {
    return "undefined";
  } else {
    return obj.constructor.name;
  }
}
```

:::note
この方法で得られる結果は、前者の方法では `integer` となるところが `Integer` に、 `string` となるところが `String` というように、プリミティブ型だと頭文字が大文字になります。
:::

# おわり

この `typeof` アップグレード版みたいの、標準にしてくれないかなぁ･･･。

</details>