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.