简体   繁体   中英

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.

Ideally, I want proxy to be of type

{
  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. 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. 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.

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. 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

// 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> . Then, if you call proxySingleFunction() , it will produce outputs of the relevant type:

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. Then you could pacakge these output functions in a single proxy object and use it:

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. 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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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