Ohar
Ohar

Reputation: 320

How to set types for a more specific function which executes a less specific function at Typescript?

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

Answers (1)

jcalz
jcalz

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);

Playground link to code

Upvotes: 1

Related Questions