This is an extension of this question
Given this code:
class Animal {
a: string;
}
class Dog extends Animal {
b: string;
}
class Foo<T>{}
function test<T,A extends Dog>(animal:A, func: (p: A) => T): T;
function test<T,A extends Animal>(animal:A, func: (p: A) => Foo<T>): Foo<T>;
function test<T,A extends Animal>(animal:A, func: (p: A) => T|Foo<T>): T|Foo<T> {
return func(animal);
}
is there a cleaner way of writing the overload that doesn't require the A type Parameter? Or maybe a cleaner of writing any of it? Basically the function conditionally calls the given func
with the given animal
. If given a dog, type T
is returned. If given some other animal, a type Foo<T>
is returned.
I was unable to get @jcalz version to work, but it took me a while to realize it was related to promises, but I'm not sure how to resolve the issue. Below is my "ugly but it works" methodology, and @jalz's "it's nice but it's broke" methodology:
class Animal {
a: string;
}
class Dog extends Animal {
b: string;
}
class Foo<T>{ }
function test<T, A extends Dog>(animal: A, func: (p: A) => Promise<T>): Promise<T>;
function test<T, A extends Animal>(animal: A, func: (p: A) => Promise<Foo<T>>): Promise<Foo<T>>;
function test<T, A extends Animal>(animal: A, func: (p: A) => Promise<T> | Promise<Foo<T>>): Promise<T | Foo<T>> {
return func(animal);
}
const foo: Promise<Foo<string>> = test(new Animal(), (a) => { return Promise.resolve(new Foo<string>()); });
const other: Promise<string> = test(new Dog(), (d) => { return Promise.resolve(d.b); });
type AnimalFunc<T> = {
(dog: Dog): Promise<T>;
(animal: Animal): Promise<Foo<T>>;
}
function test2<T>(dog: Dog, func: AnimalFunc<T>): Promise<T>;
function test2<T>(animal: Animal, func: AnimalFunc<T>): Promise<Foo<T>>;
function test2<T>(animal: Animal, func: AnimalFunc<T>): Promise<T | Foo<T>> {
return func(animal);
}
const foo2: Promise<Foo<string>> = test2(new Animal(),
(a) => {
return Promise.resolve(new Foo<string>());
}); // Errors: TS2345 Argument of type '(a: any) => Promise<Foo<string>>' is not assignable to parameter of type 'AnimalFunc<string>'.
// Type 'Promise<Foo<string>>' is not assignable to type 'Promise<string>'.
// Type 'Foo<string>' is not assignable to type 'string'.TypeScript Virtual Projects C: \_Dev\CRM\WebResources\webresources\new_\scripts\Payment.ts 498 Active
const other2: Promise<string> = test2(new Dog(), (d) => { return Promise.resolve(d.b); });
I understand
the function conditionally calls the given
func
with the givenanimal
. If given a dog, typeT
is returned. If given some other animal, a typeFoo<T>
is returned.
to mean that the parameter func
accepts all Animal
inputs but will return different types depending on whether its input is a Dog
or not. That means I would declare func
to of the following overloaded type:
type AnimalFunc<T> = {
(dog: Dog): T;
(animal: Animal): Foo<T>;
}
Then, the function test
just passes its animal
input to its func
input and returns whatever type it gets back. To get that to happen, I would declare test
like this:
function test<T>(dog: Dog, func: AnimalFunc<T>): T;
function test<T>(animal: Animal, func: AnimalFunc<T>): Foo<T>;
function test<T>(animal: Animal, func: AnimalFunc<T>): T | Foo<T> {
return func(animal);
}
Hope that helps.
@daryl said:
This works for the definition, but not call sites. If I pass in a dog as the first parameter, my function must accept a dog and return an T, else it must accept an animal and return a Foo, At the call sites, it complains the the function isn't returning the other type (T or Foo)
Without knowing all your use cases I can't tell what the best definition would be. If you really have a function of type AnimalFunc<T>
it should work:
function func1(dog: Dog): string;
function func1(animal: Animal): Foo<string>;
function func1(animal: Animal): string | Foo<string> {
if (animal instanceof Dog) {
return "woof";
}
return new Foo<string>();
};
var dog: Dog = new Dog();
var cat: Animal = new Animal();
var dogTest: string = test(dog, func1);
var catTest: Foo<string> = test(cat, func1);
If you are trying to pass in a different type of function, please spell out the use cases. Thanks.
@daryl said:
This works for the definition, but not call sites. If I pass in a dog as the first parameter, my function must accept a dog and return an T, else it must accept an animal and return a Foo, At the call sites, it complains the the function isn't returning the other type (T or Foo)
Okay, I don't think this has much to do with Promise
s. It looks like you want func
to either take a Dog
and return a Promise<T>
, or take an Animal
and return a Promise<Foo<T>>
, but not necessarily both. That is, a particular func
might only want a Dog
and will not accept a Cat
. That's not how I understood it originally.
For this case, then I'd say you want to do:
function test3<T>(dog: Dog, func: (dog: Dog) => Promise<T>): Promise<T>;
function test3<T>(animal: Animal, func: (animal: Animal) => Promise<Foo<T>>): Promise<Foo<T>>;
function test3<T, A extends Animal>(animal: A, func: (animal: A) => Promise<T> | Promise<Foo<T>>): Promise<T> | Promise<Foo<T>> {
return func(animal);
}
Note that the declarations of test3
(the top two lines) are typed for the benefit of the caller, while the implementation (the third line) is typed for the benefit of the implementer. If all you care about is type safety for people calling test3
but are secure enough in your implementation that you don't need TS to verify the types for you, then you can just implement it as:
function test3<T>(dog: Dog, func: (dog: Dog) => Promise<T>): Promise<T>;
function test3<T>(animal: Animal, func: (animal: Animal) => Promise<Foo<T>>): Promise<Foo<T>>;
function test3(animal: any, func: any): any {
return func(animal); // fine, but even return animal(func) would be accepted here, to disastrous results at runtime
}
The implementation signature with the generic A
is about as specific as I think I can get. It accepts any type of animal A
for animal
, and a function func
that will definitely accept animal
and return either a Promise<T>
or a Promise<Foo<T>>
. This is safe enough for calling func(animal)
, but you could still fool the typechecker by having an implementation like
function test3<T, A extends Animal>(animal: A, func: (animal: A) => Promise<T> | Promise<Foo<T>>): Promise<T> | Promise<Foo<T>> {
return Promise.resolve(new Foo<T>()); // no error!!
}
which would cause problems with the first overload declaration since it doesn't ever return a Promise<T>
.
I hope this has helped.