Issue
I need an invoker function. This function will invoke factories for class constructions of a given base class. The base class has a generic parameter that is used as the constructors first and only argument type.
class ServiceBase<TArg = unknown> {
constructor(protected arg: TArg) {}
}
class NumberService extends ServiceBase<number> {}
Now to the factory function. The factory function should be an inline function without any typings. It takes one Argument. The Argument type should be infered by the base class generic argument of the class that is constructed.
Example:
invokeFactory((arg) => new NumberService(arg))
Because the factory provided to invokeFactory
returns a NumberService
and it is extending ServiceBase<number>
the arg needs to be infered to type number
.
I tried to infer the Service type and Arg type, this is easy and is working. When the factory type definition types the arg as any.
Test code:
const invokeFactory = <
TService extends ServiceBase,
TArg = TService extends ServiceBase<infer T> ? T : never
>(
factory: (arg: any /* Dont use TArg*/) => TService
): [TService, TArg] => {
const magicArg: TArg = null!; // for simplification
return [factory(magicArg), magicArg];
};
const [numberService, arg] = invokeFactory((x) => new NumberService(x));
numberService
is infered as NumberService
and arg
is infered as number
.
As soon as I explicitly specify the arg type of the function instead of using any, TService is not infered correctly:
const invokeFactory = <TService extends ServiceBase>(
factory: (arg: TService extends ServiceBase<infer T> ? T : never) => TService
): [TService, TService extends ServiceBase<infer T> ? T : never] => {
const magicArg: TService extends ServiceBase<infer T> ? T : never = null!; // for simplification
return [factory(magicArg), magicArg];
};
const [numberService, arg] = invokeFactory((x) => new NumberService(x));
numberService
is infered to ServiceBase<unknown>
and arg
to unknown
Is there any way this can be done?
Solution
This is a limitation or missing feature of TypeScript; simultaneous generic type argument inference and contextual typing of function callback parameters doesn't always succeed. Whether or not it works depends strongly on the exact order in which TypeScript analyzes the expressions. Many common cases are handled already, but there are plenty of situations in which things go wrong. There's a catch-all open GitHub issue at microsoft/TypeScript#47599, and there has been progress, but unless TypeScript's inference algorithm is completely overhauled (see microsoft/TypeScript#30134) there will always be holes like this somewhere.
Generally speaking the type of an expression of the form x => f(x)
or x => new C(x)
depends on the type of x
. Functions and constructors can be generic or overloaded, and the output type can therefore depend on the input. Right now the compiler doesn't do any more detailed analysis to determine whether or not this is actually the case for a given f
or C
.
You would like
invokeFactory(x => new NumberService(x))
to be analyzed as follows:
- The type of the expression
x => new NumberService(x)
depends on the type ofx
, but maybe the return type doesn't. Let's check: - The
NumberService
constructor always produces aNumberService
instance, regardless of its constructor arguments. (but this analysis does not happen) - Therefore, yes, the expression
new NumberSevice(x)
is of typeNumberService
regardless of the type ofx
- Therefore
x => new NumberService(x)
returnsNumberService
. - Therefore
TService
should be inferred asNumberService
. - Therefore
TService extends ServiceBase<infer T> ? T : never)
isnumber
. - Therefore
x => new NumberService(x)
is of type(arg: number) => NumberService
- Therefore the callback parameter
x
is contextually typed asnumber
. - Therefore
new NumberService(x)
is acceptable.
Unfortunately that's not the order that things happen in. It's more like:
- The type of the expression
x => new NumberService(x)
depends on the type ofx
, but maybe the return type doesn't depend on the input type. Let's check: - Uh oh, the expression
new NumberService(x)
involvesx
. That's a circular dependency so I have to put off evaluation of this until I know whatx
is. x
is contextually typed by(arg: TService extends ServiceBase<infer T> ? T : never)
, so I need to evaluate that type.- In order to do that, I need to infer
TService
. - Oops, there are no expressions involving
TService
whose types I already know. There is no inference site. I'll have to fallback to the constraint ofServiceBase
, equivalent toServiceBase<unknown>
. - Therefore
TService extends ServiceBase<infer T> ? T : never
is of typeunknown
. - Therefore
x
is of typeunknown
. - Therefore
new NumberService(x)
is unacceptable.
Perhaps someday this situation will be improved for your particular example. But again, the general class of problem will remain. In any case, you'll need to handle this yourself, either by explicitly specifying the type argument like invokeFactory<NumberService>(x => new NumberService(x))
or by explicitly annotating the callback parameter like invokeFactory((x: number) => new NumberService(x))
or some other way to cut the knot.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.