🎄

Mapped Typesを活用する / TypeScript一人カレンダー

2022/12/20に公開

こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2022の9日目です。昨日は『Extract<T, U>Exclude<T, U>』を紹介しました。

Mapped Types

以前inferを紹介したように、Utility Typesはいくつかの基礎的なAPIの組み合わせによって実現が可能です。ここまでに紹介してきたPick<T, K>Omit<T, K>も例外ではなく、それぞれ自作することができます。

それでは、Pick<T, K>を紹介した際には触れなかった定義の内部について確認してみましょう。

https://github.com/microsoft/TypeScript/blob/ff919e3b76c096547a4a33d7632102382eb025cd/lib/lib.es5.d.ts#L1580

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

ここで出てきたのが[P in K]: T[P];をいう書き方です。これはMapped Typesとして紹介されています。Mapped TypesはTypeScript 2.1で誕生しました。

https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#handbook-content

MappedはArray.prototype.map()のMapと同じように捉えてください。すなわち、特定の型の集まりから、別の型の集まりを作りたいという用途で採用します。

例を紹介します。与えられた文字を繰り返して返す関数double()をイメージしてください。

function double(arr: string[]): string[] {
  return arr.map((v) => `${v}${v}`);
}

console.log(double(["A", "B", "C"]));
// ["AA", "BB", "CC"]

console.log(double(["XX", "YY", "ZZ"]));
// ["XXXX", "YYYY", "ZZZZ"]

ECMAScriptとして実行されるこの関数は、なんら不思議なものではありません。では、同様に与えられたリテラルを2回繰り返して返すようなMapped Typeを定義してみましょう。

type Double<T extends { [p: string]: string }> = {
  [P in keyof T]: `${T[P]}${T[P]}`
}

type T1 = Double<{ a: "A"; b: "B"; c: "C"}>
//   ^? { a: "AA"; b: "BB"; c: "CC"; }

type T2 = Double<{ x: "XX"; y: "YY"; z: "ZZ"}>
//   ^? { x: "XXXX"; y: "YYYY"; z: "ZZZZ"}

このように"A""AA", "ZZ""ZZZZ"になるような型Double<T>を定義することができました。

どうやって定義する?

ではMapped Typesの定義方法について解説していきます。

Double<T extends { [p: string]: string }>T型パラメータをとる、Double型のジェネリクスの宣言部分です。T型はextends { [p: string]: string }のため、なんらかの文字列プロパティと文字列型をそなえたオブジェクトである必要があります。

そして[P in keyof T]:部分は、T型に渡されたオブジェクトの全キーをUnionとして列挙するというもので、P inは列挙を一つ一つP型パラメータで扱うという意味です。arr.map(v => )との立場関係を比較すると、変数arrkeyof Tが同じ立場、引数vP型が同じ立場です。

その後の部分、${T[P]}${T[P]}は、文字列リテラル型を2回繰り返す操作をしている箇所です。T[P]によって、Tに渡されたオブジェクトからPプロパティの型を取り出しており、それを2回繰り返すように記述しています。"A""ZZ"が得られるため、それを${"A"}${"A"}${"ZZ"}${"ZZ"}のようにしているということです。

Template Literal Types

${T[P]}のような書き方を意外に思われた方もいるかもしれません。これはTemplate Literal Typesとしてドキュメントに掲載されているAPIのひとつで、実はECMAScriptのテンプレートリテラルさながらの操作ができます。

https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html#handbook-content

Pick<T, K>の中身をみてみる

いまの理解でもう一度Pick<T, K>を見てみましょう。

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

中身をみると、前節の例で取り上げた[P in keyof T]ではなく[P in K]であるのが特徴です。つまりここではT型のオブジェクトのキーすべてではなく、Kにて列挙されたプロパティのみをマップしていることがわかります。よって、Kに指定したプロパティのみを扱うオブジェクト型が得られるという結果になるのでした。

この記事をもとに、もしまだPick<T, K>の紹介をご覧になっていない方がいれば、ぜひそちらもどうぞ。

明日は『readonlyReadonlyArray<T>

本日紹介したMapped Typesは、Conditional Typesと双璧をなすUtility Typesを構成する大きなAPIです。これらを共に理解したら、TypeScriptの練度はかなり高まっているといってよいでしょう。

明日からはMapped Typesを支えるその周辺のAPIや型とその実例について紹介していきます。それではまた。

Discussion