TypeScript で関数型の返り値型を後から限定したい
Q&A
質問
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