はじめに
TypeScriptで、一部のプロパティだけが動的に変わるようなオブジェクトの型定義について、Mapped Types, インデックスシグネチャ, インターセクション型を用いて定義してみました。
最終的なコードは下記になります。
type FixedProps = {
[key in 'prop1' | 'prop2' | 'prop3']: { value: string }
}
type DynamicProps = {
[key: string]: {
value1?: string,
value2?: number,
}
}
type Props = FixedProps & DynamicProps
Mapped Typesとは
Mapped Typesは、ある型をベースに新しい型を作る機能です。
RequiredやOmitなどのutility typeの内部でも使われています。
例えば、Requiredの定義は下記のようになっています。
type Required<T> = {
[P in keyof T]-?: T[P];
};
引数で渡される型Tのプロパティが、keyofとinによって一つずつ型変数Pに入り、-?でオプショナルを外し、T[P]によって該当のプロパティの値の型を再定義しています。
定義を見て少し思ったのが、引数Tに対してextendsで{ [key: string]: any }とかobjectを指定して、引数をオブジェクトのみに限定するのもアリなのかなと思いました。
type Required<T extends { [key: string]: any }> = {
[P in keyof T]-?: T[P];
};
// Type 'string' does not satisfy the constraint '{ [key: string]: any; }'
type sample = Required<string>
少し話しが逸れましたが、今回は固定のプロパティの部分で、プロパティの値の型が同じであり、繰り返しの記述を避けるため使いました。
下記の部分になります。
type FixedProps = {
[key in 'prop1' | 'prop2' | 'prop3']: { value: string }
}
inによってユニオン型の値が1つずつ取り出されkeyとして定義されます。
最終的には下記のような型になります。
type FixedProps = {
prop1: { value: string },
prop2: { value: string },
prop3: { value: string }
}
インデックスシグネチャとは
インデックスシグネチャは、プロパティ名を指定せず、プロパティ名の型と値の型のみを指定する機能です。
例えば、keyがstring型で値がすべてnumber型の定義は下記のようになります。
type IndexSignature = {
[key: string]: number
}
keyは型変数なので任意の名前をつけられますが、keyやKで定義されることが多いです。
また、keyの型には、string, number, symbol, テンプレートリテラル型の4つを指定できます。
例えば、テンプレートリテラル型を使ってsample_ではじまるプロパティだけに制限する場合は下記のようになります。
type TemplateLiteralPropType = { [key: `sample_${string}`]: string };
const sampleObj: TemplateLiteralPropType = {
sample_key: 'a',
sample_key2: 'b',
key3: 'c', // Object literal may only specify known properties, and 'key3' does not exist in type 'TemplateLiteralPropType'.
}
最初に定義した一部動的なプロパティをもつオブジェクトの型においては、動的に変わるプロパティの部分で使用しています。
type DynamicProps = {
[key: string]: {
value1?: string,
value2?: number,
}
}
この場合、プロパティのkeyの型がstringで、値がvalue1とvalue2という任意のプロパティを持つオブジェクトになります。
こうすることで、動的なプロパティの値を使いたい時などに型推論が効くようになります。
下記の変数sampleの型が上記のDynamicPropsの場合

インターセクション型とは
インターセクション型は、型同士をまとめて新しい型を作る機能です。
オブジェクト型同士の場合
オブジェクト型に対してインターセクション型を使う場合は、それぞれのオブジェクトのプロパティをもつ新たな型が定義されます。
type Obj1 = {
a: string,
b: number,
}
type Obj2 = {
c: boolean,
}
type IntersectionType = Obj1 & Obj2
// 下記のようになる
// type IntersectionType = {
// a: string,
// b: number,
// c: boolean,
// }
プリミティブ型同士の場合
異なるプリミティブ型に対してインターセクション型を使うと、never型になります。
type Primitive = string & number
// type Primitive = never
プリミティブ型で構成されたユニオン型同士の場合
プリミティブ型で構成されたユニオン型同士に対してインターセクション型を使うと、共通するプリミティブ型のみの型になります。
type PrimitiveUnionType1 = string | number
type PrimitiveUnionType2 = string | boolean
type IntersectionType = PrimitiveUnionType1 & PrimitiveUnionType2
// type IntersectionType = string
これは一つ前で説明したプリミティブ型同士のインターセクション型はnever型になるという性質のためです。
上記の場合、PrimitiveUnionType1とPrimitiveUnionType2の組み合わせは下記の4通りです。
string & string // => string
string & boolean // => never
number & string // => never
number & boolean // => never
上記のように、プリミティブ型で構成されたユニオン型同士に対してインターセクション型を使う場合、共通するプリミティブ型以外の場合はnever型になるため、共通するプリミティブ型のみの型になります。
一部動的に変わるプロパティを持つオブジェクトの型定義
以上のMapped Types, インデックスシグネチャ, インターセクション型を用いて一部のプロパティだけが動的に変わるようなオブジェクトの型定義をしてみました。
type FixedProps = {
[key in 'prop1' | 'prop2' | 'prop3']: { value: string }
}
type DynamicProps = {
[key: string]: {
value1?: string,
value2?: number,
}
}
type Props = FixedProps & DynamicProps
もっと良い方法や間違いなどあればご指摘いただけると幸いです!