TypeScriptのMapped Typesでのプロパティの型指定方法いろいろ

前回、ちらっとMapped Typesのプロパティの型を extends で切り替える方法を書いたが、他にもいろいろな指定方法があるのでメモ。

環境

TypeScript v4.7.4 で確認。

実装例

以下のtypeを用意。

type ExampleType = {
  one: string
  two: number
  three: boolean
}

直接指定

最もシンプル。

type ExampleMappedType = {
  [P in keyof ExampleType]: string
}

const example: ExampleMappedType = {
  one: 'one',
  two: 'two',
  three: 'three',
}

Mapped Typesの記法は { [P in K]: T } だが、直接指定の場合 Record<K, T>に置き換えられる。

Conditional Types

前回書いたが、Conditional Typesが使える。

type ExampleMappedType = {
  [P in keyof ExampleType]: P extends 'one' ? string : number
}

const example: ExampleMappedType = {
  one: 'one',
  two: 2,
  three: 3,
}

P を用いた型指定

{ [P in K]: T }P を用いた T の型指定が可能。

プロパティ参照

ブラケット表記法によるプロパティ参照ができる。

type ExampleMappedType = {
  [P in keyof ExampleType]: ExampleType[P]
}

const example: ExampleMappedType = {
  one: 'one',
  two: 2,
  three: false,
}

参照する型定義は、 P に含まれるプロパティが存在すればいいため、 [P in K]K の型である必要はない。

type ReferenceType = Record<keyof ExampleType | 'four', string>

type ExampleMappedType = {
  [P in keyof ExampleType]: ReferenceType[P]
}

const example: ExampleMappedType = {
  one: 'one',
  two: 'two',
  three: 'three',
}
関数の引数や戻り値

関数にすることで、 P を引数や戻り値の型に使える。

as によるKey Remappingでプロパティ名の変換と合わせて使うと、プロパティ名に対応した関数を宣言できる。

type ExampleMappedType = {
  [P in keyof ExampleType as `format${Capitalize<P>}`]: (value: ExampleType[P]) => string
}

const example: ExampleMappedType = {
  formatOne: value => `one: ${value}`,
  formatTwo: value => `two: ${value}`,
  formatThree: value => `three: ${value}`,
}

Key Remapping中にConditional Typesの判定を挟むことも可能。

type ExampleMappedType = {
  [P in keyof ExampleType as P extends 'three'
    ? `is${Capitalize<P>}`
    : `get${Capitalize<P>}`]: () => ExampleType[P]
}

/* これでも同じ
type ExampleMappedType = {
  [P in keyof ExampleType as `${P extends 'three'
    ? 'is'
    : 'get'}${Capitalize<P>}`]: () => ExampleType[P]
}
*/

const example: ExampleMappedType = {
  getOne: () => 'one',
  getTwo: () => 2,
  isThree: () => true,
}

参考

以下のページが詳しいが、使われている用語が公式ハンドブックにまとめられていない模様。

zenn.dev

www.typescriptlang.org

型引数 P, K, T の説明に使われている用語は、Mapped types追加のPRから取ったものか?

github.com

homomorphic mapped type は、TypeScriptのissueやFAQに出てくる。

github.com

Stack Overflowにも質問があった。

stackoverflow.com

振り返り

Mapped Typesの型指定時にConditional Typesとブラケット表記を使ったところ、これまたコードレビューで「なにこれ?」と聞かれたので書いてみた。

Conditional Typesは、型を指定できるところでは当然のように書けるので、いつの間にやら使っていたが、Mapped Typesと組み合わせて使う例はWeb検索してもあまり出てこない気がする。