BobtheMagicMoose
BobtheMagicMoose

Reputation: 2429

How to convert switch statement to object literal in TypeScript

I am trying to convert a switch statement to object literal in typescript but am getting the following error:

Argument of type 'AllowedAnimals' is not assignable to parameter of type 'never'.
  The intersection 'Dog & Horse' was reduced to 'never' because property 'species' has conflicting types in some constituents.
    Type 'Dog' is not assignable to type 'never'. 

... with the example code is below. Am I doing my typings incorrectly or is this anti-pattern with Typescript?

interface Animal{
    species:string,
    weight:number,
    name:string
}

type AllowedAnimals = Dog |Horse

interface Dog extends Animal{
    species:"dog",
    hasFleas:boolean
}

interface Horse extends Animal{
    species:"horse",
    handsTall:number
}

const processDog = (d:Dog)=>{}

const processHorse = (h:Horse)=>{}

const userInput = "";

const a:AllowedAnimals = JSON.parse(userInput);

switch (a.species){
    case "dog":
        processDog(a);
        break;
    case "horse":
        processHorse(a);
        break;
    default:
        break;
}

const objectLiteral = {
    "dog":processDog,
    "horse":processHorse
}

objectLiteral[a.species](a); //<- Error

Upvotes: 1

Views: 2266

Answers (2)

Bergi
Bergi

Reputation: 664936

Typescript isn't clever enough to figure out that the type of objectLiteral[a.species] is dependent on the concrete type of a. All it knows that the property access will come up with (d: Dog) => void | (h: Horse) => void, but when you try to call that it will need to unify the argument types, arguing that the argument would need to have a type that can be passed to any of these functions. (a: Dog & Horse) => void comes up as a: never, as the error message explains.

You can cheat however with an explicit cast:

(objectLiteral[a.species] as (a: AllowedAnimal) => void)(a);

That way you can satisfy the typechecker. Whether it's a good idea I can't tell - you probably get less complete checks than with switch.

type process<X extends Animal> = (a: X) => void
const processDog: process<Dog> = _=>{}
const processHorse: process<Horse> = _=>{}

const objectLiteral: {
   [p in AllowedAnimals['species']]: process<AllowedAnimals & {species: p}>
} = {
    "dog": processDog,
    "horse": processHorse
};

let a: AllowedAnimals = null as any;
(objectLiteral[a.species] as process<AllowedAnimals>)(a);

Upvotes: 3

Mahdi Ghajary
Mahdi Ghajary

Reputation: 3253

As I said in my comment:

Typescript is doing fine here. a is a value which you're telling typescript is of type AllowedAnimals which itself is a union. when you write objectLiteral[a.species] typescript doesn't know which one between dog or horse would be indexed so it can't give you a hint about the function argument.

As you have mentioned in your question, you could benefit from type narrowing using switch statements to do what you're trying to do:

interface Animal{
    species:string,
    weight:number,
    name:string
}

type AllowedAnimals = Dog |Horse

interface Dog extends Animal{
    species:"dog",
    hasFleas:boolean
}

interface Horse extends Animal{
    species:"horse",
    handsTall:number
}

const processDog = (d:Dog)=>{}

const processHorse = (h:Horse)=>{}

const process = (animal : AllowedAnimals) => {
    switch(animal.species) {
        case "dog":
            objectLiteral[animal.species](animal);
            break;
        case "horse":
            objectLiteral[animal.species](animal);
            break;
    }
}

const objectLiteral = {
    "dog":processDog,
    "horse":processHorse
}

const userInput = "";

const a:AllowedAnimals = JSON.parse(userInput);

process(a);

Playground.

Upvotes: 2

Related Questions