メインコンテンツまでスキップ

Mapped Types

インデックス型では設定時はどのようなキーも自由に設定できてしまい、アクセス時は毎回undefinedかどうかの型チェックが必要です。入力の形式が決まっているのであればMapped Typesの使用を検討できます。

Mapped Typesは主にユニオン型と組み合わせて使います。ここにサポートする言語を定義します。

ts
type SystemSupportLanguage = "en" | "fr" | "it" | "es";
ts
type SystemSupportLanguage = "en" | "fr" | "it" | "es";

これをインデックス型と同じようにキーの制約として使用することができます。

ts
type Butterfly = {
[key in SystemSupportLanguage]: string;
};
ts
type Butterfly = {
[key in SystemSupportLanguage]: string;
};

このようにButterflyを定義するとシステムがサポートしない言語、ここではdeが設定、使用できなくなります。

ts
const butterflies: Butterfly = {
en: "Butterfly",
fr: "Papillon",
it: "Farfalla",
es: "Mariposa",
de: "Schmetterling",
Object literal may only specify known properties, and 'de' does not exist in type 'Butterfly'.2353Object literal may only specify known properties, and 'de' does not exist in type 'Butterfly'.
};
ts
const butterflies: Butterfly = {
en: "Butterfly",
fr: "Papillon",
it: "Farfalla",
es: "Mariposa",
de: "Schmetterling",
Object literal may only specify known properties, and 'de' does not exist in type 'Butterfly'.2353Object literal may only specify known properties, and 'de' does not exist in type 'Butterfly'.
};

Mapped Typesを使ったユーティリティ型の紹介とその実現方法

プロパティを読み取り専用にするreadonlyをそのオブジェクトのすべてのプロパティに適用するReadonly<T>というユーティリティ型があります。

📄️ Readonly<T>

全プロパティを読み取り専用にする

Readonly<T>もこの機能で実現されています。Readonly<T>は次のように実装されています。

ts
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
ts
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

keyof Tという見慣れない表現が登場しましたが、これはオブジェクトのキーをユニオン型に変更するものだと解釈してください。keyofの詳細は型演算子をご覧ください。

📄️ keyof型演算子

keyofはオブジェクトの型からプロパティ名を型として返す型演算子です。たとえば、nameプロパティを持つ型に対して、keyofを使うと文字列リテラル型の"name"が得られます。

mapping modifier

-を先頭につけ-readonlyとすることで、逆に読み取り専用となっているプロパティを変更可能にするMutable<T>を作ることもできます(これはユーティリティ型にはありません)。このときの-をmapping modifierと呼びます。

ts
type ImmutableButterfly = Readonly<Butterfly>;
type MutableButterfly = {
-readonly [key in SystemSupportLanguage]: string;
};
 
const immutableButterfly: ImmutableButterfly = {
en: "Butterfly",
fr: "Papillon",
it: "Farfalla",
es: "Mariposa",
};
 
immutableButterfly.en = "Schmetterling";
Cannot assign to 'en' because it is a read-only property.2540Cannot assign to 'en' because it is a read-only property.
 
const mutableButterfly: MutableButterfly = {
en: "Butterfly",
fr: "Papillon",
it: "Farfalla",
es: "Mariposa",
};
 
mutableButterfly.en = "Schmetterling"; // OK
ts
type ImmutableButterfly = Readonly<Butterfly>;
type MutableButterfly = {
-readonly [key in SystemSupportLanguage]: string;
};
 
const immutableButterfly: ImmutableButterfly = {
en: "Butterfly",
fr: "Papillon",
it: "Farfalla",
es: "Mariposa",
};
 
immutableButterfly.en = "Schmetterling";
Cannot assign to 'en' because it is a read-only property.2540Cannot assign to 'en' because it is a read-only property.
 
const mutableButterfly: MutableButterfly = {
en: "Butterfly",
fr: "Papillon",
it: "Farfalla",
es: "Mariposa",
};
 
mutableButterfly.en = "Schmetterling"; // OK

mapping modifier(-)は他にもオプション修飾子の前につけて-?とすることで、オプション修飾子を取り除くことができます。これを使うことで、Partial<T>の逆の効果を持つRequired<T>を実装することができます。

📄️ Partial<T>

全プロパティをオプショナルにする

📄️ Required<T>

全プロパティを必須にする

インデックスアクセスの注意点

{ [K in string]: ... }のようにキーにstringなど、リテラル型でない型を指定した場合は、インデックスアクセスに注意してください。存在しないキーにアクセスしても、キーが必ずあるかのようにあつかわれるためです。

次の例のように、{ [K in string]: number }型のdictオブジェクトには、aキーはあるのに対し、bキーはありません。しかし、dict.bnumberとして推論されます。

ts
const dict: { [K in string]: number } = { a: 1 };
dict.b;
number
ts
const dict: { [K in string]: number } = { a: 1 };
dict.b;
number

実際のdict.bの値はundefinedになるので、もしもdict.bのメソッドを呼び出すと実行時エラーになります。

ts
const dict: { [K in string]: number } = { a: 1 };
console.log(dict.b);
undefined
dict.b.toFixed(); // 実行時エラーが発生する
ts
const dict: { [K in string]: number } = { a: 1 };
console.log(dict.b);
undefined
dict.b.toFixed(); // 実行時エラーが発生する

このような挙動は、型チェックで実行時エラーを減らしたいと考える開発者にとっては不都合です。

この問題に対処するため、TypeScriptにはコンパイラオプションnoUncheckedIndexedAccessが用意されています。これを有効にすると、インデックスアクセスの結果の型がT | undefinedになります。つまり、undefinedの可能性を考慮した型になるわけです。そのため、dict.bのメソッドを呼び出すコードはコンパイルエラーになり、型チェックの恩恵が得られます。

ts
const dict: { [K in string]: number } = { a: 1 };
dict.b;
number | undefined
dict.b.toFixed();
'dict.b' is possibly 'undefined'.18048'dict.b' is possibly 'undefined'.
ts
const dict: { [K in string]: number } = { a: 1 };
dict.b;
number | undefined
dict.b.toFixed();
'dict.b' is possibly 'undefined'.18048'dict.b' is possibly 'undefined'.

📄️ noUncheckedIndexedAccess

インデックス型のプロパティや配列要素を参照したときundefinedのチェックを必須にする

Mapped Typesには追加のプロパティが書けない

Mapped Typesは追加のプロパティが定義できません。ここは、インデックス型とは異なる点です。

ts
type KeyValuesAndName = {
[K in string]: string;
name: string; // 追加のプロパティ
A mapped type may not declare properties or methods.7061A mapped type may not declare properties or methods.
};
ts
type KeyValuesAndName = {
[K in string]: string;
name: string; // 追加のプロパティ
A mapped type may not declare properties or methods.7061A mapped type may not declare properties or methods.
};

追加のプロパティがある場合は、その部分をオブジェクトの型として定義し、Mapped Typesとインターセクション型を成す必要があります。

ts
type KeyValues = {
[K in string]: string;
};
type Name = {
name: string; // 追加のプロパティ
};
type KeyValuesAndName = KeyValues & Name;
ts
type KeyValues = {
[K in string]: string;
};
type Name = {
name: string; // 追加のプロパティ
};
type KeyValuesAndName = KeyValues & Name;

上の例は、ひとつの型にまとめることもできます。

ts
type KeyValuesAndName = {
[K in string]: string;
} & {
name: string; // 追加のプロパティ
};
ts
type KeyValuesAndName = {
[K in string]: string;
} & {
name: string; // 追加のプロパティ
};