Generic 関数の Wrapper 関数の戻り値の型を正しく導出する方法

# 概要

TypeScript のとあるGeneric関数のWrapper関数を書いた際に、引数の型から戻り値の型が正しく導出できない問題に遭遇しました。本記事はその対処の記録です。

本記事のコードは [TypeScript Playground](https://www.typescriptlang.org/play?target=6#code/PTAEHkCcEsHNoHYEMA2oBmBXBBjALtAPYIBQeAngA4CmoAIknkuOg0wDwAqAfKALygA3iVCgAJoyQAuUJ1AAfUNjHV0iamJIBfEmSq0AgpFhskXXgOGiA2gGlQiUAGtq5QulkBdAPwzTLUy47T25tXQoaUAAlagBnTBQ8QJ5+IRFQOwcEZ1d3LxkrUXFJGU5g9NFCPAALakhYmSQEcnSdHT1Io1hzVMKcVBQAIyQcJ19QAAoJJhkY+MTk7gBKfl4AN0JoTVFp6VAuxbCSHGJYvAxsgS4AGm4JwVB+lCGRp2viplAtGS7zFb5eH1TudqAg8DA4qlwIMAFbUfAAOlB4OgcSmkhWSFiGRcbg8nHe-lYkiCuLynBCnmsnl0ohOCDOoEg1HOAmhcMR6EghAAtgBRMEQ2ITCqgZFChE8pCUCYTay495rVCef68eWud4PXYyJUoBG7d5VWr1GQARgATABmAAsXxV6Ux2LmCSSJJ4tMeA2Go28CImzLwSw9AcwkGyAaOIFAAHVINKaJBJqag-TGQB3OOUBOm1ITJDGGQABXzBFQ7GLcZ5LLqsXYEWoeXQCG41gADCFVWlRCGwxd7qKEYP87BrqLtV2iqJ0IRCAVRZPxw9dZhqGarV9519R5PRIOEcP9ZJRTpRFog+0o7H43VJuaU8CxTzBhoVGJ2b10ovRdPZxOFyUhFAZdV1AC1LQ3ScTw3dp62iOJCBQNZqE4fQegEORqAADzwUExGxX5EHQG8AFVeG8UBiNAGQEGoJDIF0VNzgza9IHNVIbjuYcfmMP5ZjiF1klAAAyODYgQpCUJoOt9DyagnxfDR2W4CxAXSHtsibftJz3YdtyKPc5OfMRX3ZPSdgAwptKHYxDyYMz9MHQyFLfWFbKQY9WkdUSBLdYTRPE5DUPrWT5OMxTYWUsIgA) でも参照可能です。


# Wrap対象のGeneric関数

```ts
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関数 - 問題あり版


```ts
const wrapper = (arg: Partial<Parameters<typeof fn>[0]>) => {
  return fn({
    ...arg,
    data: {
      foo: {
        data: { value: 123 }
      },
      ...arg.data
    }
  })
}
```

引数 arg.data に複数の個所で同じオブジェクトを渡す必要があったため、そのオブジェクトを埋め込んだ Wrapper 関数を書きました。ここで問題発生。
戻り値の型をチェックすると、、、

```ts
ResultData<unknown>
```

型Tが unknown になってしまっています。これでは `foo.data` にアクセスできません。

# Wrapper関数 - 解決版

```ts
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>>
}
```
戻り値の型
```ts
ResultData<T & {
  foo: {
    value: number
  }
}>
```

## 解説

まず、埋め込むオブジェクトの型を取得するため、オブジェクトを関数の外に出します。
```ts
const embeddedObj = {
  data: {
    foo: {
      data: { value: 123 }
    }
  }
}
```

次に、wrapper の戻り値の型を次のように明示。
```ts
ResultData<T & ResolveType<typeof embeddedObj>>
```

Wrapper関数の型引数Tと `embeddedObj` を組み合わせて `ResultData<T>` の型引数としています。ここのキモは `ResolveType<T>`。

```ts
type ResolveType<T> = T extends Arg<infer U> ? U : never
```

`ResultData<T>` の `T` は引数のオブジェクトそのものではなく、`Arg<T>` によって決定されるものなので、`infer` を使って型を導出しています。

# まとめ

複雑なGeneric関数を書くと型の取得に困ることがよくありますが、今回のように infer を使うことで解決する場面もあるので、本記事を頭の片隅にとどめておいていただければ幸いです。