「TypeScript の型の種類と関係・構造が分からん…」ってなったので、ドキュメントとプレイグラウンドを使いながら整理していく。
例えば「object と {} の違いが分かるか?」って話。
JavaScript のプリミティブ型・オブジェクト型
TypeScript は JavaScript の Typed Superset なので、まずは JavaScript の型に注目する。
7 種類のプリミティブデータ型があります。
他のすべてのものはオブジェクト型と呼ばれます。
MDN の情報から JavaScript のデータ型は以下のように分類できる。
- JavaScript のデータ型
JavaScript のラッパーオブジェクト
JavaScript において2 のプリミティブ型とは、プロパティを持たないデータのことを指す。
a primitive (primitive value, primitive data type) is data that is not an object and has no methods or properties.
「プロパティを持たないデータ」とあるが、JavaScript は文字列に対して "hoge".split("og") のようにメソッドを呼び出すことができる。
これはラッパーオブジェクトのプロパティにアクセスすることで実現している。
プリミティブ値のプロパティにアクセスすると JavaScript はプリミティブの型に対応したラッパーオブジェクト (文字列の場合は String) でラップ (auto-box) して、
そのオブジェクトのメソッド (値として関数を持つプロパティ) を呼び出している。
各プリミティブ型に対応したラッパーオブジェクトが存在するが、 null と undefined にはない。
TypeScript のプリミティブ型・非プリミティブ型
TypeScript においてもっとも広い型 (もっとも値を受け入れられる型) は
任意の値を割り当てられる
any または unknown と言える。
unknwon を typeof value === "object" で narrowing すると、以下に分類される(playground)。
object|null{}|undefined
const value: unknown = {};
if (typeof value === "object") {
value;
// ^? const value: object | null
} else {
value;
// ^? const value: {} | undefined
}
object は non-primitive を表現する型。
そのため、さらに typeof value === "string" で narrowing すると never 型になる (playground)。
const value: unknown = {};
if (typeof value === "object") {
if (typeof value === "string") {
value;
// ^? const value: never
}
}
null は typeof では "object" になる 3 が、TypeScript としても、JavaScript としても プリミティブ型。
これらを考慮すると以下のように書ける。
- プリミティブ? (
{}|null|undefined) - 非プリミティブ (
object)
…{} ってオブジェクトじゃん…?
{} 型
{} は null と undefined 以外の任意の型を表す 4。
では null と undefined は何?という話だが、前述の通り、 「ラッパーオブジェクトがないプリミティブ型」であり、プロパティにアクセス出来ない型と言い換えることもできる。
つまり、{} 型は「任意のプロパティを持ちうる型」とも言える。
{} は明示的なプロパティを持たないので、任意のプロパティを持ちうるオブジェクトを受け入れられる。
// 任意のプロパティを持ちうる形なので、オブジェクトを受け入れられる。
let empty: {};
empty = {}; // ✅
empty = { hoge: 1 }; // ✅
empty = 1; // ✅
empty = Symbol(); // ✅
empty = null; // ❌
empty = undefined;
// 明示的にプロパティを持つ型は、余計なプロパティを持てない
let nonEmpty: { fuga: 1 };
nonEmpty = {}; // ❌
nonEmpty = { hoge: 1 }; // ❌
nonEmpty = 1; // ❌
nonEmpty = Symbol(); // ❌
nonEmpty = null; // ❌
nonEmpty = undefined; // ❌
任意の型なので、{} に対してプリミティブ型で narrowing しても else block で never 型を作ることが出来ない。
const value = {} as unknown;
if (typeof value !== "object") {
value;
// ^? const value: {} | undefined
if (
value === undefined ||
typeof value === "string" ||
typeof value === "number" ||
typeof value === "bigint" ||
typeof value === "boolean" ||
typeof value === "symbol"
) {
value;
// ^? const value: string | number | bigint | boolean | symbol | undefined
} else {
value;
// ^? const value: {} 👈️🤔
}
}
Footnotes
-
オブジェクト型に属する型の一覧をここに書くことはやめる。標準組み込みオブジェクト など、種類が多すぎる。 ↩
-
他の言語も同じようなものかもしれないが、この記事を書くにあたって調べていない。 ↩
-
typeof null === "object";になるのは、JavaScript の初期の実装に基づいている (参考) ↩ -
{}型の定義・説明を TypeScript のドキュメントから見つけられないので、contributor の issueComment と typescript-eslint を参考にする。 ↩