简体   繁体   English

在 TypeScript 映射类型中保留泛型

[英]Preserving generics in TypeScript mapped types

I have a class, whose instance methods are handlers, each of which represents an operation, taking a reference as the input and assign the output to the second parameter.我有一个类,它的实例方法是处理程序,每个处理程序代表一个操作,将引用作为输入并将输出分配给第二个参数。 A proxy object is generated by a third party library so that the handlers can be called directly.代理对象由第三方库生成,以便可以直接调用处理程序。

type InputRef<T> = {
  current: T,
};
type OutputRef<T> = {
  current?: T,
};

class Original {
  increment(input: InputRef<number>, output: OutputRef<number>) {
    const { current: inValue } = input;
    
    output.current = inValue + 1;
  }
}

type Mapper<Fn> = Fn extends (input: InputRef<infer U>, output: OutputRef<infer V>) => unknown ? (input: U) => V : never;
type MyProxyGeneratedByThirdPartyJs = { [FnName in keyof Original]: Mapper<Original[FnName]> };

declare const proxy: MyProxyGeneratedByThirdPartyJs;

const result = proxy.increment(3); // 4

However, the mapper does not work when the handler involves generic types, eg,但是,当处理程序涉及泛型类型时,映射器不起作用,例如,

class Original {
  toBox<T>(input: InputRef<T>, output: OutputRef<{ boxed: T }>) {
    const { current: inValue } = input;

    output.current = { boxed: inValue };
  }
}

Using the same way above, the type of proxy only involves unknown , and the generic information T is lost.使用上面相同的方式, proxy的类型只涉及unknown ,而通用信息T丢失了。

Ideally, I want proxy to be of type理想情况下,我希望proxy是类型

{
  toBox<T>(input: T): { boxed: T },
}

instead of代替

{
  toBox(input: unknown): { boxed: unknown },
}

Is there any way achieving this?有什么办法可以做到这一点?

This is not currently possible in TypeScript.这在 TypeScript 中目前是不可能的。 In order to express what you're doing to generic functions at the type level, you'd need higher kinded types as requested in microsoft/TypeScript#1213 , but these are not directly supported.为了在类型级别表达您对泛型函数所做的操作,您需要更高种类的类型,如microsoft/TypeScript#1213中所要求的,但这些类型不受直接支持。 The conditional type definition of Mapper<Fn> infers U as the input type and V as the output type, but there is no capacity for the compiler to see or represent any higher-order relationship between them when Fn is generic. Mapper<Fn>条件类型定义将U推断为输入类型,将V推断为输出类型,但当Fn是泛型时,编译器无法查看或表示它们之间的任何高阶关系。

There is some support for transforming generic function types into other generic function types , but this only happens in very specific circumstances.一些支持将泛型函数类型转换为其他泛型函数类型,但这只发生在非常特殊的情况下。 The transformation needs to happen at least partially at the value level (there must be a function value which transforms one generic function type into another when called, not just the type of that function) so there will be JS code emitted.转换至少需要部分发生在值级别(必须有一个函数在调用时将一种通用函数类型转换为另一种类型,而不仅仅是该函数的类型),因此将发出 JS 代码。 And the transformation only works for a single function at a time, so an object of functions cannot be mapped at once without losing the generics.并且转换一次只适用于单个函数,因此在不丢失泛型的情况下不能一次映射一个函数对象。

Still, to show that there is some ability to do this, here is how one might approach it:尽管如此,为了表明有一定的能力做到这一点,这里是一个可能的方法:

function proxySingleFunction<U, V>(
    f: (input: InputRef<U>, output: OutputRef<V>) => any
): (input: U) => V {
    return function (input: U) {
        const o: OutputRef<V> = {};
        f({ current: input }, o);
        const ret = o.current;
        if (ret === undefined) throw new Error("OH NO");
        return ret;
    }
}

The type of proxySingleFunction() is proxySingleFunction()的类型是

// function proxySingleFunction<U, V>(
//   f: (input: InputRef<U>, output: OutputRef<V>) => any
// ): (input: U) => V

which looks similar to what you're doing with Mapper<Fn> .这看起来与您使用Mapper<Fn>所做的类似。 Then, if you call proxySingleFunction() , it will produce outputs of the relevant type:然后,如果您调用proxySingleFunction() ,它将产生相关类型的输出:

const increment = proxySingleFunction(Original.prototype.increment);
// const increment: (input: number) => number

const toBox = proxySingleFunction(Original.prototype.toBox);
// const toBox: <T>(input: T) => { boxed: T; }

You can see that toBox is generic, as desired.您可以看到toBox是通用的,正如所希望的那样。 Then you could pacakge these output functions in a single proxy object and use it:然后您可以将这些输出函数打包到一个proxy对象中并使用它:

const proxy = {
    increment, toBox
}

console.log(proxy.increment(1).toFixed(1)) // "2.0"
console.log(proxy.toBox("a").boxed.toUpperCase()) // "A"

So that's great and it works.所以这很好,而且很管用。 But it requires that you emit JavaScript for each method you want to transform.但它要求您为每个要转换的方法发出 JavaScript。 If you already have such a transformed object from a third party and just want to represent the typings, the closest you can get is to lie to the compiler via type assertions so that it thinks you're doing the transformations when you're actually not:如果您已经从第三方获得了这样一个转换后的对象,并且只想表示类型,那么您最接近的方法就是通过类型断言对编译器撒谎,这样它就认为您在进行转换,而实际上您并没有这样做:

// pretend this function exists
declare const psf: <U, V>(
    f: (input: InputRef<U>, output: OutputRef<V>) => any
) => (input: U) => V;

// pretend you're using it to create a proxy object
const myProxyType = (true as false) || {
    increment: psf(Original.prototype.increment),
    toBox: psf(Original.prototype.toBox)
}

// get the pretend type of that object
type MyProxyGeneratedByThirdPartyJs = typeof myProxyType;
/* type MyProxyGeneratedByThirdPartyJs = {
  increment: (input: number) => number;
  toBox: <T>(input: T) => { boxed: T; };
} */

I don't see this as a big win over just writing out these types manually in the first place, so I don't know that I'd recommend it.我不认为这比一开始就手动写出这些类型有很大的好处,所以我不知道我会推荐它。 It's just the closest I can imagine getting to what you want with the language as it currently is.这只是我能想象到的最接近您想要的语言的当前状态。

Playground link to code 游乐场代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM