I'm trying to specify specific return types based on the values of properties passed to a method.
This works with a single signature, however fails when I try to define different overloads restricted on parameter values.
class Cat{}
class Dog{}
type MyType = {
// This works
//call: ((method: 'cat') => Cat)
// this errors
call: ((method: 'cat') => Cat) | ((method: 'dog') => Dog)
};
var s:MyType = {
call(method:string): Cat | Dog
{
return new Cat()
}
}
// err: Argument of type 'string' is not assignable to parameter of type 'never'
let result = s.call('cat');
Is it possible to achieve what I'm after?
Thanks in advance.
CodePudding user response:
You've got your variance backward.
call: ((method: 'cat') => Cat) | ((method: 'dog') => Dog)
This says "call
is either a function that takes the string 'cat'
or a function that takes the string 'dog'
". There's no way to know which type of function it is, so we can never call it. Typescript performed the following simplifications.
((method: 'cat') => Cat) | ((method: 'dog') => Dog)
((method: 'cat') => Cat | Dog) | ((method: 'dog') => Cat | Dog)
(method: 'cat' & 'dog') => Cat | Dog
(method: never) => Cat | Dog
The first simplification is by covariance of return type. A function that returns Cat
can be said to return a supertype (Cat | Dog
), and the same of a function which returns Dog
. Then we have distributivity of arguments. If we have a value of type ((A) => R) | ((B) => R)
, then that value can only be called with an argument that is both an A
and a B
, hence what we really have is (A & B) => R
. Finally, 'cat' & 'dog'
is never
since there are no strings that are equal to both 'cat
' and 'dog'
simultaneously.
Consider using an intersection instead.
call: ((method: 'cat') => Cat) & ((method: 'dog') => Dog)
This way, call
is a function which can be called in one of two ways, and each way will produce a different return type. Typescript performs similar simplifications.
((method: 'cat') => Cat) & ((method: 'dog') => Dog)
((method: 'cat') => Cat | Dog) & ((method: 'dog') => Cat | Dog)
(method: 'cat' | 'dog') => Cat | Dog
The first simplification is the same. The second changes our intersection &
to a union |
(since argument types are contravariant), so the thing we end up with is a function that takes either 'cat'
or 'dog'
and produces something that is either a Cat
or a Dog
.