In a master router function, I am trying to detect the type of one parameter by a separate string parameter and then call the most appropriate secondary function for handling the first parameter. Example (playground link here):
interface Animal {[index: string] : number | string;}
interface Food<A extends Animal = Animal> {isFor: A;}
interface Dog extends Animal {barkVolume: number;}
interface Cat extends Animal {miceCaught: number;}
interface Gerbil extends Animal {cages: number;}
interface Snake extends Animal {length: number;}
interface Fish extends Animal {idealWaterTemp: number;}
interface Bird extends Animal {song: string;}
//Seeks to follow handling of e.g. GlobalEventHandlersEventMap in lib.dom.d.ts
interface BreedTypeMap {
"copperhead": Snake;
"garter": Snake;
"python": Snake;
"burmese": Cat;
"manx": Cat;
"persian": Cat;
"siamese": Cat;
"pug": Dog;
"poodle": Dog;
"canary": Bird;
"betta": Fish;
"guppy": Fish;
"mongolian": Gerbil;
}
//The steps involved in feeding each type of animal are quite different:
const feedSnake = function(food: Food<Snake>, breed: keyof BreedTypeMap) {/*...*/}
const feedDog = function(food: Food<Dog>, breed: keyof BreedTypeMap) {/*...*/}
const feedCat = function(food: Food<Cat>, breed: keyof BreedTypeMap) {/*...*/}
const feedGerbil = function(food: Food<Gerbil>, breed: keyof BreedTypeMap) {/*...*/}
const feedBird = function(food: Food<Bird>, breed: keyof BreedTypeMap) {/*...*/}
const feedFish = function(food: Food<Fish>, breed: keyof BreedTypeMap) {/*...*/}
//Naming a second generic type does not help, as in:
//const feedAnimal = function<B extends keyof BreedTypeMap, T extends BreedTypeMap[B]>(
// food: Food<T>,
const feedAnimal = function<B extends keyof BreedTypeMap>(
food: Food<BreedTypeMap[B]>,
breed: B,
category: BreedTypeMap[B], //absent in real use; just included for generics demonstration
) {
if(breed === 'copperhead' || breed === 'garter' || breed === 'python') {
//Here, breed is correctly narrowed to "copperhead" | "garter" | "python"
//Why can't TypeScript figure out food is of type Food<Snake>?
//Instead it gives error ts(2345):
//Argument of type 'Food<BreedTypeMap[B]>' is not assignable to parameter of type 'Food<Snake>'.
//Type 'BreedTypeMap[B]' is not assignable to type 'Snake'.
//Type 'Dog | Cat | Gerbil | Snake | Fish | Bird' is not assignable to type 'Snake'.
//Property 'length' is missing in type 'Dog' but required in type 'Snake'.
feedSnake(food, breed);
console.log(category); //type Snake | Dog | Cat | Gerbil | Bird | Fish; should be just Snake
} else if(breed === ('burmese') || breed === ('manx') || breed === ('persian') || breed === ('siamese')) {
feedCat(food, breed);
} else if(breed === ('pug') || breed === ('poodle')) {
feedDog(food, breed);
} else if(breed === ('canary')) {
feedBird(food, breed);
} else if(breed === ('betta') || breed === ('guppy')) {
feedFish(food, breed);
} else if(breed === ('mongolian')) {
feedGerbil(food);
}
}
How do I write the function signature to properly narrow the type of food, ideally without casting or the use of any?
I'd also ideally like to avoid having to manually write overload signatures, especially as that would mean separately writing type signatures for the rest of a pretty large class where this router function is found.
This has been a widely misunderstood (and fairly so) aspect of the generics system: https://github.com/microsoft/TypeScript/issues/13995. There have been many issues/PR's regarding this, but overall has consistently been declined because
There is also the reason that someone could override the parameters upon entry, or maybe they are using JS. TS is used as a complement to runtime code, and as such makes it so the developer should have runtime code to handle any possible mistakes.
You can use overloads, but this gets extra large, and unmaintainable as you might add animals or whatever else.
Regardless, my recommended workaround is inline with
@kellyscommentYou can also recast the function to the old type, if you want the old inferencing, but notice the
as any, this makes it very easy and possible for the implementation to desync from the returned type, so will have to take care with that. This is built/inspired by this answer here: https://stackoverflow.com/a/53143568/17954209View this on TS Playground
There's an excellent related answer by
@jcalz, which reiterates and adds onto some of what I've said, here: https://stackoverflow.com/a/62701483/17954209