Reputation: 320
Why do I get there an error TS2322? How to handle this case to get rid of it?
In fact “makeAnimal” function is less specific and “makeCat” is more specific and as I understand, Typescript tells me that I can’t use less specific output for a more specific function.
But I need to have a common function and bunch of more specific ones which would use it.
Help me to make this right, please.
const ANIMAL_CAT = 'cat'
const ANIMAL_DOG = 'dog'
type ICat = 'isCat'
type IDog = 'isDog'
type IAnimal = ICat | IDog
type IMakeAnimal = (type: string) => IAnimal
type IMakeCat = () => ICat
type IMakeDog = () => IDog
const makeAnimal: IMakeAnimal = (type) => {
switch (type) {
case ANIMAL_CAT:
return 'isCat'
case ANIMAL_DOG:
default:
return 'isDog'
}
}
// TS2322: Type 'IAnimal' is not assignable to type '"isCat"'.
// Type '"isDog"' is not assignable to type '"isCat"'
const makeCat: IMakeCat = () => makeAnimal(ANIMAL_CAT)
Upvotes: 1
Views: 46
Reputation: 330086
The return type of your version of makeAnimal
is just IAnimal
, which could be either ICat
or IDog
. The compiler doesn't try to simulate what happens if you call makeAnimal(ANIMAL_CAT)
to narrow the return type further. You annotated that makeAnimal
is an IMakeAnimal
, whose return type is IAnimal
, so that's what the compiler sees.
If you want to leave the function as-is, then you'll have to add the missing information back in by telling the compiler that the value returned by makeAnimal(ANIMAL_CAT)
is an ICat
. You would do that via a type assertion:
const makeCat: IMakeCat = () =>
makeAnimal(ANIMAL_CAT) as ICat; // okay
// ------------------> ^^^^^^^ assert
That works, but it's only safe because your assertion happens to be correct. The compiler can't tell if you make a mistake here; it relies on your assertion being accurate. That is, the compiler isn't verifying type safety here, you are. Case in point:
const makeCatBad: IMakeCat = () =>
makeAnimal(ANIMAL_DOG) as ICat; // no errr
// --------------------> ^^^^^^^ assert
That also compiles with no error, but you've claimed that makeAnimal(ANIMAL_DOG)
will return an ICat
. The compiler just believes you.
If you want more compiler-verified type safety, you need to refactor makeAnimal()
so that its return type can depend on its input, such as making it a generic function... and you also need to change the implementation so the compiler can understand that your logic matches the generic return type. Here's one way to do it:
interface AnimalMap {
[ANIMAL_CAT]: ICat,
[ANIMAL_DOG]: IDog
}
const makeAnimal = <K extends keyof AnimalMap>(type: K): AnimalMap[K] => ({
[ANIMAL_CAT]: 'isCat' as const,
[ANIMAL_DOG]: 'isDog' as const
}[type]);
Here I've created an AnimalMap
mapping interface that represents the relationship between the ANIMAL_CAT
/ANIMAL_DOG
string literal types and the corresponding ICat
/IDog
output types. It's just an interface, because string literal types can be used as key types for objects.
The type of makeAnimal
is now a generic function, where the type
parameter is of generic type K
which is constrained to keyof AnimalMap
, that is... ANIMAL_CAT
or ANIMAL_DOG
. And the return type of makeAnimal
is the indexed access type AnimalMap[K]
, meaning the value type of the property of AnimalMap
whose key type is K
. Essentially the call signature describes a lookup of an object property.
And indeed, I've refactored the implementation of makeAnimal
away from a switch
/case
statement and to an object property lookup. The compiler is happy with that implementation, because it sees the object as being assignable to AnimalMap
, and the key type
as being assignable to K
, so the return type is seen as being assignable to AnimalMap[K]
. If you left it as a switch
/case
, you'd find that the compiler couldn't follow the logic, since K
doesn't necessarily change just by checking type
.
Anyway, now that makeAnimal()
is generic, the return type will depend on the input type, and so makeAnimal(ANIMAL_CAT)
will cause the compiler to infer ANIMAL_CAT
for K
, and the indexed access type AnimalMap[ANIMAL_CAT]
is ICat
, so the return type will be ICat
. And thus () => makeAnimal(ANIMAL_CAT)
will be seen as assignable to IMakeCat
, as desired:
const makeCat: IMakeCat = () => makeAnimal(ANIMAL_CAT);
Upvotes: 1