482F
@482F

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

TypeScript で関数型の返り値型を後から限定したい

質問

typescript で、ある関数型 A があったとして、A の引数のより具体的な型によって A の返り値を後から限定したいです (型レベルの操作のみで)

詳細

例えば

  • E 型は数値を受け取ってそれに ! を付けた文字列を返す、
  • A 型は数値を受け取ってそれを要素とする配列を返す
  • O 型は数値を受け取って、それを num キーの値とするオブジェクトを返す

と言った関数型があった時に、
E, A, O, もしくはあらゆる任意の関数型と具体的な値 42 を渡すことで、その引数を与えた場合のより限定的な返り値の型 (E の場合は 42! 型, A の場合は 42[] 型, O の場合は {num: 42} 型) が返ってくるユーティリティクラス SomeUtilType を作成したいです

type E = <T extends number>(val: T) => `${T}!`
type A = <T extends number>(val: T) => T[]
type O = <T extends number>(val: T) => { num: T }
type fte = SomeUtilType<E, 42> // -> expected `42!`
type fta = SomeUtilType<A, 42> // -> expected `42[]`
type fto = SomeUtilType<O, 42> // -> expected `{ num: 42 }`

補足

上記を実現したい理由ですが、下記のようにオブジェクト (もしくは配列) と関数を渡すと
Array.prototype.map のように各要素に適用したうえで型もいい感じにしてくれる関数を作りたいためです

const o = { a: 41, b: 42, c: 43 } as const
const mo = someMap(o, <T extends number>(num: T): `${T}!` => `${num}!`)
/*
-> expected
  {
    readonly a: `41!`
    readonly b: `42!`
    readonly c: `43!`
  }
*/

SomeUtilType を実装しようと下記のような型を作ってみましたが、どれも完全に期待を満たすことはできませんでした・・・

試作
type SomeUtilType<F, V> = F extends (arg: V) => infer U ? U : never
type fte = SomeUtilType<E, 42> // -> `${number}!`
type SomeUtilType<F, V> = F extends <A extends V>(arg: A) => infer U
  ? U
  : never
type fte = SomeUtilType<E, 42> // -> `${number}!`
type SomeUtilType<F extends (...args: any) => any, V> = F extends (
  arg: Parameters<F>[0] & V
) => infer U
  ? U
  : never
type fte = SomeUtilType<E, 42> // -> `${number}!`
interface Func {
  value?: unknown
  ret?: unknown
  (arg: NonNullable<this['value']>): this['ret']
}
interface EFunc extends Func {
  value?: number
  ret?: `${NonNullable<this['value']>}!`
}
type SomeUtilType<F extends { ret?: any }, V> = NonNullable<
  (F & { value?: V })['ret']
>
type fte = SomeUtilType<EFunc, 42> // -> `42!`

function someMap<
  O extends { [k in string]: any },
  F extends ((...args: any) => any) & { ret?: any }
>(o: O, f: F) {
  return Object.fromEntries(
    Object.entries(o).map(([key, value]) => [key, f(value)])
  ) as { [k in keyof O]: SomeUtilType<F, O[k]> }
}
const efunc: EFunc = <T extends number>(num: T): `${T}!` => `${num}!`
const o = { a: 41, b: 42, c: 43 } as const
const mo = someMap(o, efunc)
/*
->
  {
    readonly a: `41!`
    readonly b: `42!`
    readonly c: `43!`
  }
*/

最後のものに関しては一番いい線いってるんですが、できれば都度 interface を宣言しなくても使えるようになりたいんですよね・・・

0

1Answer

someMapというやりたいことを実現できるかはわからないですが、以下のようにたとえばE型の関数を使って具体的な返り値の型を絞ることはできそうです。(再度ですがこれができたからといってsomeMapをどう実現するかまでは考えきれませんでした)

type E = <T extends number>(val: T) => `${T}!`
const eFunction: E = <T extends number>(val: T) => `${val}!`

type EReturn = ReturnType<typeof eFunction<42>>
// type EReturn = '42!'
1Like

Comments

  1. @482F

    Questioner

    ご回答ありがとうございます。
    そうなんですよ。一旦値としての関数を作ると絞り込みができるんですよね・・・
    上記から色々いじって型レベルの操作のみで絞り込みができないか試行してみようと思います。参考になります。ありがとうございました。

Your answer might help someone💌