简体   繁体   中英

Naming the return type of a generic function

To not fall victim to the XY problem, here's what I want to accomplish: I have a generic decorator creator function that returns a generic decorator function (or ES6 mixin) (like <T1, T2>(...args) => <T3>(base) => class extends base { ... }; and I want to name the type of the result depending on the generic parameters. This is so I can restrict other generic parameters (in other functions) to have been passed through that decorator at least once.

I eagerly waited for TS 2.8 and thought ReturnType would solve my problem, but I failed to be able to properly use it for this case. Naively I simply tried using ReturnType<ReturnType<typeof myMixin>> , but this doesn't work as all type parameters of myMixin are coerced to {} .

The following (simpler) example illustrates the problem:

type ClassConstructor<T> = new(...args: any[]) => T;

const mixin = <B extends ClassConstructor<HTMLElement>, T>(base: B, arg: T) => {
    return class extends base {
        prop: T = arg;
    };
};

const Test1 = mixin(HTMLElement, 10); // using inference
type Test1Prop = typeof (new Test1()).prop; // number

type MixinReturnType = ReturnType<typeof mixin>;
const Test2: MixinReturnType = mixin(HTMLElement, 10); // using explicit type annotation
type Test2Prop = typeof (new Test2()).prop; // {}

TypeScript inference correctly names the type of the instance property. As soon as I give it a name, however, it turns to garbage. Is there anything I can do?

Here's my take on what's going on. If you explicitly annotate the type of a variable with a wider type than its value, the compiler (usually*) forgets the narrower type of the value and treats the variable as the wider type only. For example:

interface General {
  foo: string;
}
interface Specific extends General {
  bar: number
}
declare function getSpecific(): Specific;
declare function acceptSpecific(x: Specific): void;

const implicitlyTyped = getSpecific();
acceptSpecific(implicitlyTyped); // okay

const explicitlyTyped: General = getSpecific();
acceptSpecific(explicitlyTyped); // error!

The explicitlyTyped variable is only understood by the compiler to be of type General , and so it is an error to call acceptSpecific(explicitlyTyped) . This is responsible for the problem where the widened MixinReturnType causes all subsequent inspections of Test2 to disappoint you.

Therefore the solution cannot be to find a version of MixinReturnType that is somehow generic/wide enough to accept all outputs of mixin() but narrow enough to preserve the exact type of each specific output of mixin() . No such type exists:

const Test2: MixinReturnType = mixin(HTMLElement, 10); 
const t2Prop = new Test2().prop;

const Test3: MixinReturnType = mixin(HTMLElement, "hey");
const t3Prop = new Test3().prop;

You want Test3 to be the same type as Test2 but you want t3Prop to be a different type from t2Prop , which just doesn't work.

* Control flow analysis will sometimes narrow a variable to string/number/boolean literals or single constituents of a union, but that is not applicable here.


Here's something that might work (depending on what you're trying to do). Use a generic helper function that just passes the input through to the output, but requires the input to be assignable to MixinReturnType , like this:

const requireMixinReturnType = <M extends MixinReturnType>(m: M) => m;

Note that this function is generic in M ; so if M is narrower than MixinReturnType , the output will also be narrower. Let's see it in action:

const Test2 = requireMixinReturnType(mixin(HTMLElement, 10));         
const t2Prop = new Test2().prop; // number

This is good, in that t2Prop is now known to be number . You might wonder how this is any better than the implicitly typed Test1 . Well, it isn't really; after all, there's not really much point in writing requireMixinReturnType(mixin(...)) . But now you do have a way to prevent someone form passing you a bad mixin type without widening the input:

const BadMixin = requireMixinReturnType(HTMLElement); // error!
// Property 'prop' is missing in type 'HTMLElement'.

So probably you wouldn't use requireMixinReturnType() unless you're trying to catch a bad type early.


More likely, you would take the function you care about that currently accepts a MixinReturnType and make it generic:

declare function doConcrete(m: MixinReturnType): void {
   const p = new m().prop; // {}, oops
}

declare function doGeneric<M extends MixinReturnType>(m: M): void {
       const p = new m().prop; // still {}, oops
}

Uh oh, TypeScript is not smart enough to take the generic M and extract the type of prop . There are some ways to deal with this. The most straightforward is to forget about MixinReturnType and instead think about the generic definition we care about. Something like this:

function doGeneric<T, E extends HTMLElement>(
  m: ClassConstructor<E & { prop: T }>
): void {
  const p = new m().prop; // T, that's good
}

In this case, we're saying that the m parameter must be a constructor that returns something of type E & {prop: T} , where E is something assignable to HTMLElement and T is anything. Now the compiler realizes that p is of type T . Let's try it out with something that returns something, like:

function getProp<T, E extends HTMLElement>(
  m: ClassConstructor<E & { prop: T }>
): T {
  return new m().prop; // type checks, yay!
}

const t2Prop = getProp(Test2); // number, yay!

That works! This was very long-winded; hope it makes sense and is of some help. Good luck!

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