はじめに
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
もっと良い方法や間違いなどあればご指摘いただけると幸いです!