t28.dev

TypeScript の primitive/non-primitive type を構造的に整理する

2025/7/7
Tech

「TypeScript の型の種類と関係・構造が分からん…」ってなったので、ドキュメントとプレイグラウンドを使いながら整理していく。 例えば「object{} の違いが分かるか?」って話。

JavaScript のプリミティブ型・オブジェクト型

TypeScript は JavaScript の Typed Superset なので、まずは JavaScript の型に注目する。

7 種類のプリミティブデータ型があります。

ref: MDN - Primitive (プリミティブ)

他のすべてのものはオブジェクト型と呼ばれます。

ref: MDN - JavaScript のデータ型とデータ構造 - オブジェクト

MDN の情報から JavaScript のデータ型は以下のように分類できる。

  • JavaScript のデータ型
    • プリミティブ型
    • オブジェクト型 1

JavaScript のラッパーオブジェクト

JavaScript において2 のプリミティブ型とは、プロパティを持たないデータのことを指す。

a primitive (primitive value, primitive data type) is data that is not an object and has no methods or properties.

ref: MDN - Primitive (プリミティブ)

「プロパティを持たないデータ」とあるが、JavaScript は文字列に対して "hoge".split("og") のようにメソッドを呼び出すことができる。 これはラッパーオブジェクトのプロパティにアクセスすることで実現している。 プリミティブ値のプロパティにアクセスすると JavaScript はプリミティブの型に対応したラッパーオブジェクト (文字列の場合は String) でラップ (auto-box) して、 そのオブジェクトのメソッド (値として関数を持つプロパティ) を呼び出している。 各プリミティブ型に対応したラッパーオブジェクトが存在するが、 nullundefined にはない

TypeScript のプリミティブ型・非プリミティブ型

TypeScript においてもっとも広い型 (もっとも値を受け入れられる型) は 任意の値を割り当てられる any または unknown と言える。 unknwontypeof value === "object" で narrowing すると、以下に分類される(playground)。

  • object | null
  • {} | undefined
const value: unknown = {};

if (typeof value === "object") {
  value;
  // ^? const value: object | null
} else {
  value;
  // ^? const value: {} | undefined
}

objectnon-primitive を表現する型。 そのため、さらに typeof value === "string" で narrowing すると never 型になる (playground)。

const value: unknown = {};

if (typeof value === "object") {
  if (typeof value === "string") {
    value;
    // ^? const value: never
  }
}

nulltypeof では "object" になる 3 が、TypeScript としてもJavaScript としても プリミティブ型。 これらを考慮すると以下のように書ける。

  • プリミティブ? ({} | null | undefined)
  • 非プリミティブ (object)

{} ってオブジェクトじゃん…?

{}

{}nullundefined 以外の任意の型を表す 4。 では nullundefined は何?という話だが、前述の通り、 「ラッパーオブジェクトがないプリミティブ型」であり、プロパティにアクセス出来ない型と言い換えることもできる。 つまり、{} 型は「任意のプロパティを持ちうる型」とも言える。

{} は明示的なプロパティを持たないので、任意のプロパティを持ちうるオブジェクトを受け入れられる。

(playground)

// 任意のプロパティを持ちうる形なので、オブジェクトを受け入れられる。
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

  1. オブジェクト型に属する型の一覧をここに書くことはやめる。標準組み込みオブジェクト など、種類が多すぎる。

  2. 他の言語も同じようなものかもしれないが、この記事を書くにあたって調べていない。

  3. typeof null === "object"; になるのは、JavaScript の初期の実装に基づいている (参考)

  4. {} 型の定義・説明を TypeScript のドキュメントから見つけられないので、contributor の issueCommenttypescript-eslint を参考にする。