LoginSignup
3
2

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

Last updated at Posted at 2024-03-23

概要

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 を使うことで解決する場面もあるので、本記事を頭の片隅にとどめておいていただければ幸いです。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2