lamechang-dev

Webフロントエンドエンジニア lamechangのブログ。

【TypeScript】レコード型とマップ型

はじめに

今回はTypeScriptにおいてObjectの型定義をしていく上で必須知識とも言っても過言ではないレコード型・マップ型についてです。花粉辛い。

レコード型

TypeScriptの組み込みのRecord 型は 、 あるものからあるものへのマップ(対応付け)としてオブジェクトを表現するための方法です。オブジェクトが特定のキー&バリューの集まりであることを強制するために Record 型は有用です。

以下のような記法を取ります。

type RockPaperScissors = "Rock" | "Paper" | "Scissors";

type Hoge = Record<RockPaperScissors, RockPaperScissors>;

const winTarget: Hoge = {
  Rock: "Scissors",
  Paper: "Rock",
  Scissors: "Paper",
};

類似の記法に インデックスシグネチャ があります。こちらも同様にオブジェクトの値の型を制約できますが、キーに採用できるのは stringnumber だけになりますね。例えば、上記の例で定義した 文字列リテラルのユニオン型である RockPaperScissors は キーに採用できません。エラーも、「代わりにmapped object type を使ってくれ」と言ってます。

このように、Record型のキーは、stringnumberのサブタイプであれば自由に制約をつけることができます。

// An index signature parameter type cannot be a union type. 
// Consider using a mapped object type instead.
const winTarget: { [key: RockPaperScissors]: RockPaperScissors } = {
  Rock: "Scissors",
  Paper: "Rock",
  Scissors: "Paper",
};

また、オブジェクトが特定のキーの集まりを定義することを強制できることもポイントです。以下のように、最初に提示した Record型の例からキーRock を消してみました。するとどうなるでしょうか?

// Property 'Scissors' is missing in type '{ Rock: "Scissors"; Paper: "Rock"; }' 
// but required in type 'Hoge'.ts(2741)
const winTarget: Hoge = {
  Rock: "Scissors",
  Paper: "Rock",
  // Scissors: "Paper",
};

有益なエラーメッセージを得られていることがわかります。これはTypeScriptにおける 完全性による網羅チェックによる挙動になります(この記事では割愛)。

マップ型

マップ型はより強力な(柔軟な)型宣言が可能になります。と言うより、そもそもTypeScriptのRecord型自体がこのマップ型を利用して実装されているため、当たり前といえば当たり前かもしれません。以下がRecord型の実際の定義です。

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

Record型の定義に登場した独自の構文がありますね。[P in K]: T記述された部分です。名前が示すように、これはオブジェクトのキー・値両方の型をマッピングするための方法です。具体的な記法の例を以下に紹介していきます。

type Pokemon = {
  skills: Array<string>;
  name: string;
  weekTypes: Array<string>;
};

// すべてのフィールドを省略可能にします type OptionalPokemon = {
type OptionalPokemon = { [K in keyof Pokemon]?: Pokemon[K] };
// すべてのフィールドをnull許容にします type NullablePokemon = {
type NullablePokemon = { [K in keyof Pokemon]: Pokemon[K] | null };
// すべてのフィールドを読み取り専用にします type ReadonlyPokemon = {
type ReadonlyPokemon = { readonly [K in keyof Pokemon]: Pokemon[K] };

このように柔軟に型情報を定義することが可能です。

組み込みのマップ型

上記に紹介したようなマップ型は実際の開発現場でも利用頻度が高いものが多いです。TypeScriptはこのような頻出と思われるマップ型の型定義があらかじめ定義されています。特にチーム開発ではこれらを採用していくことでObjectの型定義の可読性が上がると思うので、積極的に使っていきましょう。

// Keys 型のキーと Values 型の値を持つオブジェクト。
Record<Keys, Values>
// Object内のすべてのフィールドを省略可能と指定します。
Partial<Object> 
// Object内のすべてのフィールドを必須(省略不可)と指定します。
Required<Object> 
// Object内のすべてのフィールドを読み取り専用と指定します。
Readonly<Object> 
// 指定された Keysだけを持つ、Objectのスーパータイプを返します。
Pick<Object, Keys>

終わり