Reputation: 93
I am waiting for have got function from switch with required number of parameters. But it isn't working. I am waiting for fun1 0 arguments because PrintSomething() don't have arguments But have got error //Expected 1 arguments, but got 0. Some idea?
type opt = 'number1' | 'string2' | 'something3'
export class Factory {
static getFun = (option: opt) => {
switch (option) {
case 'number1':
return NumberFun;
case 'string2':
return StringFun;
case 'something3':
return PrintSomething;
}
};
}
//getResult(option: string): ((val: number) => void) | ((str: string) => void)
//but what about PrintSomething() and type ()=>void????
let fun1 = Factory.getFun('something3');
fun1()
function NumberFun(val: number) {
console.log(val);
}
function StringFun(str: string) {
console.log(str);
}
function PrintSomething() {
console.log('something');
}
Upvotes: 1
Views: 666
Reputation: 327884
The TypeScript compiler does not attempt to synthesize a function signature which represents the results of control flow analysis within the function implementation.
All it does is produce something like the union of the types of all the values returned in the implementation. For your code, this look like:
(option: Opt) => ((val: number) => void) | ((str: string) => void)
If you are wondering where () => void
went, it's because () => void
is assignable to both members of that union. See the documentation on comparing functions for details on why that is true. And so it is absorbed in the signature (like string | "a"
becomes just string
).
The rules for when and how the compiler performs subtype reductions from unions are heuristic in nature, so I don't know if I can explain exactly why they decided not to infer ((val: number) => void) | ((str: string) => void) | (() => void)
, but the point is the compiler's inferred function type is consistent with the implementation.
It's just not suitable for your intended use.
If you want Factory.getFun()
to have a strongly typed input-ouput relationship where the return type depends on the input value, you're going to have to write out this relationship yourself, and probably even assert that the value is of that type, because the compiler cannot easily verify when an implementation is or is not assignable to such types.
Your options for what this type should be look like:
You can make getFun
an overloaded type with three call signature, like this:
interface GetFunOverloaded {
(option: "number1"): typeof NumberFun,
(option: "string2"): typeof StringFun,
(option: "something3"): typeof PrintSomething
};
static getFun = ((option: Opt) => {
// impl here
}) as GetFunOverloaded;
You can make getFun
a generic function which returns a conditional type, like this:
interface GetFunConditional {
<O extends Opt>(option: O):
O extends 'number1' ? typeof NumberFun :
O extends 'string2' ? typeof StringFun :
O extends 'something3' ? typeof PrintSomething :
never;
}
static getFun = ((option: Opt) => {
// impl here
}) as GetFunConditional;
And finally, you can make getFun
a generic function which indexes into a mapping interface, like this:
interface OptMap {
number1: typeof NumberFun,
string2: typeof StringFun,
something3: typeof PrintSomething
}
interface GetFunIndexed {
<O extends keyof OptMap>(option: O): OptMap[O];
}
static getFun = ((option: Opt) => {
// impl here
}) as GetFunIndexed;
All three of those will work from the caller's side, at least for the use case you've shown here:
let fun1 = Factory.getFun('something3');
// let fun1: () => void
fun1() // okay
But of those three types for getFun
, the compiler cannot verify that the implementation matches them for at least two of them. It's a hard problem.
For overloads, there were suggestions at microsoft/TypeScript#13235 and microsoft/TypeScript#17747 to have the compiler understand that a function accurately implements a set of overloaded call signatures, or to synthesize such a set of call signatures. These were closed as being too complex for the compiler.
For generic conditional types, there's an open suggestion at microsoft/TypeScript#33912 to have the compiler understand that a function accurately implements a generic conditional type. But it's listed that it's a hard problem to solve, for similar reasons as with overloads; it's a lot of work for the compiler.
So for these you need the type assertion as GetFun
, as shown above.
The compiler is able to verify an implementation conforms to the generic indexed access signature, but not with the particular implementation using switch
. It only works if you index into an object, like this:
const optMap: OptMap = {
number1: NumberFun,
string2: StringFun,
something3: PrintSomething
}
export class Factory {
static getFun = <O extends keyof OptMap>(option: O) => optMap[option];
}
// Factory.getFun: <O extends keyof OptMap>(option: O) => OptMap[O]
let fun1 = Factory.getFun('something3');
// let fun1: () => void
fun1() // okay
Note how getFun()
is inferred by the compiler as having the same type as GetFun
. And so this would be my recommended approach for your example, since you get strong typings and type safety.
Upvotes: 3