TypeScriptの『typeof X[keyof typeof X]』の意味を順を追って理解する

JavaScript

TypeScriptのコードでたまにみかけるtype HogeType = typeof Hoge[kyof typeof Hoge]の意味について解説をします。

結論: typeof X[kyeof typeof X]の意味

typeof X[kyeof typeof X]オブジェクトXのプロパティの型をUnion型で表現します。
具体例は以下の通りです。

const Color = {
  Red: "red",
  Green: "green",
  Blue: "blue",
} as const;

type Color = typeof Color[keyof typeof Color];
// 以下のように評価される
// type Color = "red" | "green" | "blue"

特に、Enum型の代わりにUnion型を利用する際にtypeof X[kyeof typeof X]の表現がよく使われます。
Enum型とUnion型の比較は【TypeScript】Enum型からUnion型に書き換える方法と具体例で紹介しています。

typeof X[kyeof typeof X]で利用される演算子や型について

typeof X[kyeof typeof X]を使う場面では以下の演算子や型が利用されます。

  • const assertion
  • keyof
  • typeof
  • Lookup Types(ルックアップ型)

以下ではそれぞれについて解説をします。

const asssertion(constアサーション)について

const asssertion(constアサーション)とはオブジェクトや値をreadonly(再代入不可)にする機能です。
const assertionはreadonlyにしたい変数の末尾にas constをつけることで実現できます。

const asssertionの具体例は以下の通りです。

let arr = [10, 20] as const;
// 以下のように型推論される
// let arr: readonly [10, 20]

let obj = { text: "Hello" } as const;
// 以下のように型推論される
// let obj: {
//     readonly text: "Hello";
// }

// ネストしたオブジェクトの場合、再帰的にreadonlyが付与される
let taro = {
  firstName: 'Taro',
  lastName: 'Suzuki',
  birthplace: {
    country: 'Japan',
    prefecture: 'Tokyo'
  }
} as const
// 以下のように型推論される
// let taro: {
//     readonly firstName: "Taro";
//     readonly lastName: "Suzuki";
//     readonly birthplace: {
//         readonly country: "Japan";
//         readonly prefecture: "Tokyo";
//     };
// }

const assertionの利用した場合としない場合の比較は以下の通りです。

// const assertionを利用した場合
const x = { a: 1 } as const;
x.a = 3 // error

// const assertionを利用しない場合
const x = { a: 1 };
x.a = 3
console.log(x.a) // 3

keyof(index type query operator)について

keyof(index type query operator)はオブジェクトの型を受け取り、オブジェクトのプロパティで構成されたUnion型を返す演算子です。

keyofの具体例は以下の通りです。

type Person = {
  firstName: string;
  lastName: string;
};

type PersonPropertyType = keyof Person;
// 'firstName' | 'lastName'

// OK
const firstName: PersonPropertyType = "firstName";

// OK
const lastName: PersonPropertyType = "lastName";

// NG
const age: PersonPropertyType = "age";
// Type '"age"' is not assignable to type 'key

typeofについて

typeofはオペランド(被演算子、演算対象の変数のこと)の型を文字列として返す演算子です。typeof 変数を型にすると、変数の型定義が利用できます。

typeofの具体例は以下の通りです。

// プリミティブ型の場合

const hello: string = "Hello";

console.log(typeof hello);
// string

type TypeOfHello = typeof hello;
// 以下のように型推論される
// type TypeOfHello = string

// OK
const hi: TypeOfHello = "Hi";

// オブジェクトの場合

const taro = {
  name: "Taro",
  age: 20,
};

console.log(typeof taro);
// object

type TypeofTaro = typeof taro;
// 以下のように型推論される
// type TypeofTaro = {
//     name: string;
//     age: number;
// }


// OK
const jiro: TypeofTaro = {
  name: "Jiro",
  age: 22,
};

Lookup Types(ルックアップ型)について

Lookup Types(ルックアップ型)とはオブジェクトのプロパティの型を表現するものです。
Lookup Typesは『indexed access operator』とも呼ばれています。1

Lookup TypesはT[K]のように記述します。T[K]は『型Tにおけるキー(プロパティ)Kの型』を表現します。

Lookup Typesの具体例は以下の通りです。

type Person = {
  firstName: string;
  lastName: string;
};

// Person型のfirstNameキーの型
type FirstNameType = Person["firstName"];
// 以下のように型推論される
// type FirstNameType = string

// Person型のlastNameキーの型
type LastNameType = Person["lastName"];
// 以下のように型推論される
// type LastNameType = string

typeof X[kyeof typeof X]を要素分解することで意味を理解する

以下のサンプルコードのtypeof Color[keyof typeof Color]"red" | "green" | "blue"というUnion型になります。

const Color = {
  Red: "red",
  Green: "green",
  Blue: "blue",
} as const;

type Color = typeof Color[keyof typeof Color];
// 以下のように評価される
// type Color = "red" | "green" | "blue"

typeof Color[keyof typeof Color]の意味について、以下の手順で説明します。

  1. 『typeof X』について
  2. 『keyof typeof X』について
  3. 『typeof X[keyof typeof X]』について

typeof X: オブジェクトを型として定義する

typeof ColorはColorオブジェクトを型として定義します。

const Color = {
  Red: "red",
  Green: "green",
  Blue: "blue",
} as const;
// 以下のように評価される
// const Color: {
//     readonly Red: "red";
//     readonly Green: "green";
//     readonly Blue: "blue";
// }

type TypeOfColor = typeof Color
// 以下のように評価される
// type TypeOfColor = {
//     readonly Red: "red";
//     readonly Green: "green";
//     readonly Blue: "blue";
// }

keyof typeof X: プロパティをUnion型で表現する

keyof typeof Colortypeof Color、つまりColor型のプロパティをUnion型で定義します。

const Color = {
  Red: "red",
  Green: "green",
  Blue: "blue",
} as const;

type TypeOfColor = typeof Color
// 以下のように評価される
// type TypeOfColor = {
//     readonly Red: "red";
//     readonly Green: "green";
//     readonly Blue: "blue";
// }

type KeyofTypeofColor= keyof typeof Color
// 以下のように評価される
// type KeyofTypeofColor = "Red" | "Green" | "Blue"

typeof X[keyof typeof X]: プロパティの型をUnion型で表現する

typeof ColorColor型を意味します。
ですので、typeof Color[keyof typeof Color]Color型のkeyof typeof Color、つまり"Red" | "Green" | "Blue"プロパティの型を取得します。

const Color = {
  Red: "red",
  Green: "green",
  Blue: "blue",
} as const;

type KeyofTypeofColor= keyof typeof Color
// 以下のように評価される
// type KeyofTypeofColor = "Red" | "Green" | "Blue"

type Color = typeof Color[keyof typeof Color]
// 以下のように評価される
// type Color = "red" | "green" | "blue"

以上の結果、typeof Color[keyof typeof Color]"red" | "green" | "blue"というUnion型として推論されます。

typeof X[kyeof typeof X]を利用した具体例

showColorは引数のColor型の値に応じて動的なログを出力するメソッドです。

const Color = {
  Red: "red",
  Green: "green",
  Blue: "blue",
} as const;

type Color = typeof Color[keyof typeof Color];
// type Color = "red" | "green" | "blue"

const showColor = (color: Color) => {
  if (color === Color.Red) {
    console.log("赤色です");
  }
  if (color === Color.Green) {
    console.log("緑色です");
  }
  if (color === Color.Blue) {
    console.log("青色です");
  }
};

showColor(Color.Red);
// 赤色です

showColor(Color.Green);
// 緑色です

showColor(Color.Blue);
// 青色です

// OK
// 文字列リテラルを直接指定してもOK
showColor("red");
// 赤色です

// NG
// Union型に含まれてない
showColor("yellow");
// => Argument of type '"yellow"' is not assignable to parameter of type 'Color'.(2345)

参考: const assertionの命名ルール(コーディング規約)に関する個人的な考え

as constを付与した変数や、変数のプロパティ(メンバ)の命名ルールは人によって様々です。
例えば、constアサーション「as const」 (const assertion)では変数および変数のメンバはキャメルケース(camelCase)で定義していますし、TypeScript 3.4では変数名はパスカルケース(PascalCase)、メンバはキャメルケース(camelCase)で定義しています。

ですのでas constに関する明確な命名ルールはありませんが、typeof X[keyof typeof X]を利用するシチュエーションにおいてはenumをUnion型に置き換える目的でas constが利用されているため、enumと同じ命名ルールに従った方がよいというのが個人的な考えです。

スタイルガイド(コーディング規約)には「enum名・enumメンバともにパスカルケース(PascalCase)にする」という命名ルールが存在するため、本記事のas constが付与された変数名およびプロパティにも同様の命名ルールを適用しています。

まとめ

今回のまとめ
  • typeof X[kyeof typeof X]はオブジェクトXのプロパティの型をUnion型で表現する
  • as constはオブジェクトや値をreadonlyにする場合に利用する
  • keyofはオブジェクトの型を受け取り、オブジェクトのプロパティで構成されたUnion型を返す
  • typeofはオペランドの型を文字列として返す
  • T[K]は型Tにおけるキー(プロパティ)Kの型を表現する

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考