Reputation: 117
Let's say I have similar architecture to that:
type GetDog = () => { animal: string; bark: boolean };
const getDog: GetDog = () => ({ animal: 'dog', bark: true });
type GetCat = () => { animal: string; meow: boolean };
const getCat: GetCat = () => ({ animal: 'cat', meow: true });
type AnimalFactory =
| ((callback: GetDog) => ReturnType<typeof getDog>)
| ((callback: GetCat) => ReturnType<typeof getCat>);
const calmAnimalFactory: AnimalFactory = (callback) => {
// Some fancy stuff
return callback();
};
calmAnimalFactory
as argument should accept getDog
function or getCat
function, and then returns corresponding value. The problem is that I guess type inference does not work as I supposed. Callback inside calmAnimalFactory
doesn't infer type of GetDog | GetCat
, but instead is any
. I thought Typescript should determine type of calmAnimalFactory
in a way that calmAnimalFactory(getDog)
should be typed as
((callback: GetDog) => ReturnType<typeof getDog>)
and calmAnimalFactory(getCat)
should be typed as
((callback: GetCat) => ReturnType<typeof getCat>)
I hope it's possible to achieve, but I don't know why it's not working that way.
Upvotes: 1
Views: 84
Reputation: 328453
There are a few things going on here. First of all, I think if I have to keep typing typeof getXXX
or ReturnType<typeof getXXX>
, I will go crazy, so I will define new Dog
and Cat
interfaces that correspond to what getDog()
and getCat()
return, and use those exclusively:
interface Dog extends ReturnType<GetDog> { };
interface Cat extends ReturnType<GetCat> { }
You could have and probably should have defined these Dog
and Cat
types first and then written getCat()
and getDog()
in terms of them:
interface Dog {
animal: string;
bark: boolean;
}
type GetDog = () => Dog;
const getDog: GetDog = () => ({ animal: 'dog', bark: true });
interface Cat {
animal: string;
meow: boolean;
}
type GetCat = () => Cat;
const getCat: GetCat = () => ({ animal: 'cat', meow: true });
But either way will work.
Next, if you want an AnimalFactory
to return a Cat
when you pass it a GetCat
parameter, and you want it to return a Dog
when you pass it a GetDog
parameter, then you want to use an intersection (&
) and not a union (|
). If you use a union, you're saying that an animal factory will either be something that turns GetCat
into a Cat
, or it will be something that turns GetDog
into a Dog
, but not necessarily both. So we want the intersection here:
type AnimalFactory =
((callback: GetDog) => Dog)
& ((callback: GetCat) => Cat);
Next, the compiler cannot reason particularly well about whether or not an unannotated and unasserted function implementation conforms to such an intersection of signatures. Such reasoning would be equivalent to being able to properly type-check overloaded functions, which the compiler simply cannot do (see this comment in microsoft/TypeScript#35338 for example).
So if you just try to contextually type the callback
parameter, the compiler will fail, and end up implicitly using any
, as you saw.
It would be better to use a generic function implementation that claims to be able to take callback
of type () => T
for generic T extends Cat | Dog
, and then produce a T
. That is something the compiler can type check:
const calmAnimalFactory: AnimalFactory =
<T extends Cat | Dog>(callback: () => T) => {
return callback();
};
The compiler is able to verify that a function of type <T extends Cat | Dog>(callback: () => T) => T
is assignable to both call signatures of AnimalFactory
, and therefore it compiles with no error.
In fact, depending on your use case, you might want to make AnimalFactory
as universal as possible by removing the dependency on Cat
and Dog
, and instead come up with some base type like Animal
, such as:
interface Animal {
animal: string;
}
type UniversalAnimalFactory = <T extends Animal>(callback: () => T) => T;
const universalAnimalFactory: UniversalAnimalFactory = callback => callback();
A UniversalAnimalFactory
can do anything that an AnimalFactory
can do (GetCat
in, Cat
out; and GetDog
in, Dog
out) but it can also do anything else for animals you haven't even thought of yet:
const getFish = () => ({ animal: "fish", swim: true });
universalAnimalFactory(getFish).swim // works
Upvotes: 3
Reputation: 42218
You can get proper typing on this using generics. We say that calmAnimalFactory
depends on a generic T
that represents the callback type. This T
must be a function that takes zero arguments and returns anything.
Now we can say that the callback
argument for calmAnimalFactory
is T
and the return type of the function is the same as the return type of the callback.
const calmAnimalFactory = <T extends () => any>(callback: T): ReturnType<T> => {
// Some fancy stuff
return callback();
};
const dog = calmAnimalFactory(getDog); // type: { animal: string; bark: boolean; }
const cat = calmAnimalFactory(getCat); // type { animal: string; meow: boolean; }
A slightly different version of the same idea would be to let generic T
refer to the type of the created animal. This makes it easier to require that the the callback's return must fit some base Animal
interface.
interface Animal {
animal: string;
}
const calmAnimalFactory = <T extends Animal>(callback: () => T): T => {
// Some fancy stuff
return callback();
};
We still get the same types for the cat
and the dog
.
Upvotes: 2