【TypeScript】オブジェクトの型を Generics で動的に決める
はじめに
こんにちは、FarStep です。
TypeScript を書いている皆さん、Generics 使っていますか?
今回は、Generics を使った型定義の実践的な例をご紹介します。
本記事は、10 分以内で読み終えることができます。
Generics(ジェネリクス)とは
まずはじめに、Generics(ジェネリクス)とは何でしょうか。
Generics は、型決定を遅延することができる 仕組みです。
これだけでは、あまりにも簡単な説明ですので、一つ例を示しましょう。
interface Identity<T> {
value: T
}
Identity 型を定義しました。
Generics を使っているため、value
の型は、後で決定することができます。
また、Generics を利用する際、下記の四つが慣例的によく使われますので、覚えておきましょう。
- T:Type
- K:Key
- U:Unknown
- E:Element
Identity 型を適用する際には、下記のように記述します。
interface Identity<T> {
value: T
}
const identityStr: Identity<string> = { value: "myString" }
const identityNum: Identity<number> = { value: 123 }
Identity<string>
とすると、value の型は string 型になります。
また、Identity<number>
とすると、value の型は number 型になります。
したがって、Generics を使って指定した型以外の値を value にセットするとエラーが吐かれます。
interface Identity<T> {
value: T
}
const identityNum: Identity<number> = { value: "myNumber" }
// Type 'string' is not assignable to type 'number'.
Generics がどんなものかについて、理解できたでしょうか。
上記のコードの通り、Generics は 型における変数のような機能 を果たしていますね。
Generics を使ってオブジェクトの型を動的に決める
それでは、Generics を使った実践的な例をご紹介します。
実務でも使う場面があるかと思います。
想定する場面
今回は、オブジェクトの値を取り出す関数の引数「全て」に型をつけたい という場面を想定します。
TypeScript を書いている中で、オブジェクトの操作する際、型がつかなくなるときがよくありますよね。これからこの問題を解決していきます。
まずは、想定する場面をコードに落とし込みます。
const obj = {
userA: {
email: "farstep@example.com",
verified: true,
age: 23,
},
userB: {
email: "stepfar@example.com",
verified: false,
},
}
const extractValue = (obj, first, second) => {
return obj[first][second]
}
ユーザ情報が格納されたオブジェクト(obj
)があります。
ただし、userA
と userB
とでは、存在するプロパティが異なっています。
userA
には、age
というプロパティがありますが、userB
にはありませんね。
そして、extractValue
関数は、
- オブジェクト
- 一つ目のキー
- 二つ目のキー
という3つの引数を受け取り、指定されたキーの値を返します。
しかし、現在のコードでは、引数の型を指定していないため、下記のようなエラーが吐かれます。
const extractValue = (obj, first, second) => {
return obj[first][second]
}
// Parameter 'obj' implicitly has an 'any' type.
// Parameter 'first' implicitly has an 'any' type.
// Parameter 'second' implicitly has an 'any' type.
引数の全ての値が any 型になってしまっています。
any 型使うと、型チェックが弱くなりバグを発見できなくなる恐れがあります。一般的に、TypeScript を書くのであれば、any 型は避けるべきです。
引数の型が指定されていないと、下記のようなコードを記述してしまうかもしれません。
const obj = {
userA: {
email: "farstep@example.com",
verified: true,
age: 23,
},
userB: {
email: "stepfar@example.com",
verified: false,
},
};
const extractValue = (obj, first, second) => {
return obj[first][second];
};
const value = extractValue(obj, "userB", "age");
userB
には、age
というプロパティが無いのにも関わらず extractValue(obj, "userB", "age")
に対してエラーは吐かれません。
一つ目のキーで指定した値によって、二つ目のキーに型をつけることはできないのでしょうか 🤔
しかし、一つ目のキーで指定した値によって、型が変わってしまうのです... 😱
この問題を解決するのが、Generics です。
Generics の使い方
Generics を使うことで extractValue
関数の引数である二つ目のキー(second
)の型を、一つ目のキー(first
)によって動的に変えることができます。
下記のように Generics を使います。
const obj = {
userA: {
email: "farstep@example.com",
verified: true,
age: 23,
},
userB: {
email: "stepfar@example.com",
verified: false,
},
};
const extractValue = <
TObj,
TFirst extends keyof TObj,
TSecond extends keyof TObj[TFirst]
>(
obj: TObj,
first: TFirst,
second: TSecond
) => {
return obj[first][second];
};
const value = extractValue(obj, "userA", "age");
extractValue
関数の中身を詳しく見てみます。
extractValue
関数の第一引数である obj
は TObj 型 となっています。このようにすることで、どんな型のオブジェクトも受け付けることができます。
そして、extractValue
関数の第二引数である first
は TFirst 型 となっています。TFirst 型は、TFirst extends keyof TObj
と定義されているため、第一引数で受け取ったオブジェクトのキーのみ に制限されます。つまり、今回の例ですと、first
には、"userA"
もしくは "userB"
のみを指定することができます。
最後に、extractValue
関数の第三引数である second
は TSecond 型 となっています。TSecond 型は、TSecond extends keyof TObj[TFirst]
と定義されているため、第二引数で受け取ったキーの値のキーのみ に制限されます。つまり、今回の例ですと、第一引数 first
が、"userA"
だった場合、第二引数で受け取ることができるのは、"email"
と "verified"
と "age"
です。しかし、第一引数 first
が "userB"
だった場合、第二引数で受け取ることができるのは、"email"
と "verified"
のみとなります。
ちなみに、keyof
はオブジェクト型からプロパティ名を型として返す型演算子です。
このように Generics を使えば extractValue
関数の引数の型を動的に決定することができるのです。例えば、下記のようなコードを記述するとエラーが吐かれるようになります。
const value = extractValue(obj, "userB", "age");
// Argument of type '"age"' is not assignable to parameter of type '"email" | "verified"'
extractValue
関数の第二引数において "userB"
を渡したため、Generics により、second
の型は obj["userB"]
のキーのみとなりました。なぜなら、second
は TSecond extends keyof TObj[TFirst]
と指定されているからです。
Generics を使うことで、関数の第一引数と第二引数の値に補完が効くようになります。
第一引数には、"userA"
もしくは "userB"
しか指定することができません。
第一引数に "userA"
を渡した場合、第二引数の補完の候補は三つです。
第二引数に "userB"
を渡した場合、第二引数の補完の候補は二つです。
さらに嬉しいことに、extractValue
関数で返される値(value
)も型推論が有効になっているという点です。
上記のように、オブジェクトの型を Generics で動的に決定することができました 🎉
存在しないプロパティを指定した場合には、コンパイルエラーが吐かれるようになったため、よりバグの少ないコードを書くことができます。
おわりに
いかがだったでしょうか。
Generics を使うことで 動的に オブジェクトの型を定義することができます。
動的に型を定義できる とはすなわち、型の共通化 に成功しているということです。
通常、コードの共通化すると型の安全性が弱まり、型の安全性を高めるとコードの共通化が難しくなるのですが、Generics は見事にこの問題を解決します。
是非、Generics を上手に使ってみてください。
参考文献
今回使用したコードは、下記 URL に残しておきます。
Discussion