概要
TypeScript のとあるGeneric関数のWrapper関数を書いた際に、引数の型から戻り値の型が正しく導出できない問題に遭遇しました。本記事はその対処の記録です。
本記事のコードは TypeScript Playground でも参照可能です。
Wrap対象のGeneric関数
type DataOfData<T> = {
data: T | undefined
}
type ArgData<T> = {
[K in keyof T]?: DataOfData<T[K]>
}
type ResultData<T> = {
[K in keyof T]: {
data: T[K]
others: any
}
}
type Arg<T> = {
callback?: (data: ResultData<T>) => void
data: ArgData<T>
}
const fn = <T,>({ callback, data }: Arg<T>) => {
const entries = Object.entries(data) as [keyof T, DataOfData<T[keyof T]>][]
const ret = Object.fromEntries(
entries.map(([key, val]) => [key, { data: val.data, others: 1234 }])
) as ResultData<T>
callback?.(ret)
return ret
}
Genericオブジェクト Arg<T>
を受け取り、それを加工して戻り値を生成しています。つまり、引数に与える型によって戻り値の型が決まる。
Wrapper関数 - 問題あり版
const wrapper = (arg: Partial<Parameters<typeof fn>[0]>) => {
return fn({
...arg,
data: {
foo: {
data: { value: 123 }
},
...arg.data
}
})
}
引数 arg.data に複数の個所で同じオブジェクトを渡す必要があったため、そのオブジェクトを埋め込んだ Wrapper 関数を書きました。ここで問題発生。
戻り値の型をチェックすると、、、
ResultData<unknown>
型Tが unknown になってしまっています。これでは foo.data
にアクセスできません。
Wrapper関数 - 解決版
const embeddedObj = {
data: {
foo: {
data: { value: 123 }
}
}
}
type ResolveType<T> = T extends Arg<infer U> ? U : never
const wrapper = <T,>(arg: Arg<T>): ResultData<T & ResolveType<typeof embeddedObj>> => {
return fn({
...arg,
...embeddedObj,
data: {
...arg.data,
...embeddedObj.data
}
}) as ResultData<T & ResolveType<typeof embeddedObj>>
}
戻り値の型
ResultData<T & {
foo: {
value: number
}
}>
解説
まず、埋め込むオブジェクトの型を取得するため、オブジェクトを関数の外に出します。
const embeddedObj = {
data: {
foo: {
data: { value: 123 }
}
}
}
次に、wrapper の戻り値の型を次のように明示。
ResultData<T & ResolveType<typeof embeddedObj>>
Wrapper関数の型引数Tと embeddedObj
を組み合わせて ResultData<T>
の型引数としています。ここのキモは ResolveType<T>
。
type ResolveType<T> = T extends Arg<infer U> ? U : never
ResultData<T>
の T
は引数のオブジェクトそのものではなく、Arg<T>
によって決定されるものなので、infer
を使って型を導出しています。
まとめ
複雑なGeneric関数を書くと型の取得に困ることがよくありますが、今回のように infer を使うことで解決する場面もあるので、本記事を頭の片隅にとどめておいていただければ幸いです。