ジェネリック入門
ジェネリックは、型を変数として型定義に渡すということを簡単に言えば、関数に引数を渡すのと同じようなものです。例えば:
function identity<Type>(arg: Type): Type {
return arg
}
let output = identity<string>('myString')
<>
でType
を囲むことで、ジェネリック関数に型を渡すことができます。上記の関数は、Type
型の引数を受け取り、Type
型の結果を返します。
Type
は引数なので、名前も自由ですが、一般的にはT
という名前を使用することがよくあります。したがって、以下のように書くこともできます:
function identity<T>(arg: T): T {
return arg
}
型の推論
ジェネリック関数を使用する際には、T
の型を明示的に指定する必要はありません(実際、通常はそうします)。この場合、TS はT
の型を自動的に推論します:
let output = identity('myString')
// outputもstring型です
先ほどの例でも、型<string>
を明示的に指定しない場合、TS は"myString"
の型をT
として推論するため、関数の戻り値も文字列になります。
型の制約
デフォルトでは、ジェネリックは任意の型になりますが、これでは可読性が低くなりますし、ジェネリックを適用した型に対して操作やメソッドの呼び出しを行う際には、任意の型であるため必ずチェックに通過しません。この問題を解決するために、extends
を使用してジェネリックに制約を設けることができます。
interface Lengthwise {
length: number
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length) // argは必ずlengthプロパティを持つため、型チェックを通過します
return arg
}
上記のコードでは、<T extends Lengthwise>
によってT
がlength
プロパティを持つ型であることを示しています。length
プロパティを持つ任意の型は、このジェネリックの要件を満たします。例えば、配列でも可能です。
バインディング能力
公式ウェブサイトによると、ジェネリックは型の再利用に使用されると述べていますが、上記の簡単な説明を見ると、これは非常に効果的であることがわかるでしょう。しかし、ジェネリックは型の再利用だけでなく、他の用途もあります。
私の答えは、型の連動です。T は同じ型定義内で使用される他のジェネリックにバインドすることができます。
再度、この例を見てみましょう。実際には、入力の型と出力の型をバインドしています:
function identity<Type>(arg: Type): Type {
return arg
}
以下は、より明確な「型のバインディング」の例です。
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}
let x = { a: 1, b: 2, c: 3, d: 4 }
getProperty(x, 'a') // 可能
getProperty(x, 'm') // エラー、`Key`は`Type`のキーにバインドされるため、`m`は`Type`のキーではありません
マッピング型
const myMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
}
type MyMap = typeof myMap
type MyKey = keyof MyMap
キーがa
、b
、c
であり、値が異なる関数であるオブジェクトがあるとします。このオブジェクトのキーと対応する関数の引数のオブジェクトの型を取得する必要がある場合、どのように実現できるでしょうか?
const myMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
}
function wrapper<K extends keyof typeof myMap>(key: K, fn: (typeof myMap)[K]) {
return async function (...arg: Parameters<typeof fn>) {
// do something
await Promise.resolve()
fn.apply(null, arg)
}
}
const wrappedMap: {
[K in keyof typeof myMap]: ReturnType<typeof wrapper>
} = {} as any
for (const key in myMap) {
const k = key as keyof typeof myMap
wrappedMap[k] = wrapper(k, myMap[k])
}
この回答では、ジェネリック K
を使用して key
のタイプを制限し、それが myMap
のキーであることを確認しています。その後、typeof myMap[K]
を使用して、myMap
中の対応するキーの関数タイプを取得しています。wrapper
関数では、Parameters<typeof fn>
を使用して関数 fn
のパラメータータイプを取得しています。最後に、マップ型 [K in keyof typeof myMap]: ReturnType<typeof wrapper>
を使用して、wrappedMap
のタイプを定義し、そのキーと値が myMap
と対応していることを確認しています。TypeScript は wrappedMap
のタイプを推測できないため、型チェックを回避するために as any
を使用しています。
const myMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
}
type MyMap = typeof myMap
type MyKey = keyof MyMap
function wrapper<K extends MyKey, T extends MyMap[K]>(_key: K, fn: T) {
return async function (...arg: Parameters<T>) {
await Promise.resolve()
;(fn as any).apply(null, arg)
}
}
type WrappedMap = {
[K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>
}
const wrappedMap: Partial<WrappedMap> = {}
for (const key in myMap) {
const k = key as MyKey
wrappedMap[k] = wrapper(k, myMap[k])
}
現在、WrappedMap
の型はラッパーの戻り値の効果を持っていることは確かですが、(fn as any).apply(null, arg)
という部分は突然現れたように見えますね。
なぜ fn
を any
にする必要があるのでしょうか?
なぜなら、TS にとって a
、b
、c
は値の引数の型と結びついていないからです。したがって、T
を使用して制約をかけても効果がありません。少しわかりにくいかもしれませんが、次の回答 2 を見ればより明確になるかもしれません。
回答 2
const myMap: MyMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
}
interface MyMapArgs {
a: []
b: [someString: string]
c: [someNumber: number]
}
type MyMap = {
[K in keyof MyMapArgs]: (...args: MyMapArgs[K]) => void
}
type MyKey = keyof MyMap
function wrapper<K extends MyKey, F extends MyMap[K]>(_key: K, fn: F) {
return async function (...arg: Parameters<F>) {
await Promise.resolve()
fn.apply(null, arg)
}
}
type WrappedMay = {
[K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>
}
const wrappedMap: Partial<WrappedMay> = {}
for (const key in myMap) {
const k = key as MyKey
wrappedMap[k] = wrapper(k, myMap[k])
}
(fn as any)
を削除する解決策は、まず関連付けたい要素を別のマップにマッピングし、上記の MyMapArgs
のようにします。そして、このマッピングを使用して MyMap
を作成します。これにより、TS はついにこれらの 2 つの要素が関連していることを理解します。
P.S. より詳細な情報については、issues#30581 と pull#47109 を参照してください。
この記事は ssshooter.com で初めて公開されました。