4

I want to define a function returns different type of object based on the key I give. It's basically like the trick used here in createElement function

https://github.com/Microsoft/TypeScript/blob/master/lib/lib.dom.d.ts#L3117

However, instead of string literal, I want to use string enum type instead. So I wrote something like this

class Dog {}
class Cat {}
class Bird {}

enum Kind {
  Dog = 'Dog',
  Cat = 'Cat',
  Bird = 'Bird'
}

interface KindMap {
  [Kind.Dog]: Dog
  [Kind.Cat]: Cat
  [Kind.Bird]: Bird
}

function getAnimal<K extends keyof KindMap> (key: K): KindMap[K] {
  switch (key) {
    case Kind.Dog:
      return new Dog()
    case Kind.Cat:
      return new Cat()
    case Kind.Bird:
      return new Bird()
  }
}

However, TypeScript seems doesn't like the way I put the enum Kind's value inside interface as the computed property, it complains

A computed property name in an interface must directly refer to a built-in symbol.

Here comes the question, I already have the constants defined in the enum, I don't like to use string literal, is there a way I can make this works? Which means use the Kind enum's value as the computed property key in the KindMap.

Fang-Pen Lin
  • 13,420
  • 15
  • 66
  • 96
  • 3
    [This is not supported](https://github.com/Microsoft/TypeScript/issues/16258) in current typescript version, looks like [it might be fixed in 2.7](https://github.com/Microsoft/TypeScript/pull/15473) – artem Oct 19 '17 at 02:49

2 Answers2

5

Sometimes a simple object with keyof is simpler than an enum in TypeScript:

class Dog { }
class Cat { }

const kindMap = {
    Dog,
    Cat
};

type KindMap = typeof kindMap;

function getAnimalClass<K extends keyof KindMap>(key: K): KindMap[K] {
    return kindMap[key];
}

// These types are inferred correctly
const DogClass = getAnimalClass('Dog'); 
const dog = new DogClass();

const CatClass = getAnimalClass('Cat');
const cat = new CatClass();

Try it in TypeScript Playground

I tried to implement getAnimal() with mapped types but seemed to run into buggy inference. But looking up the class was easier.

Inlining the getAnimalClass lookup also works with type inference:

const dog = new kindMap['Dog']();
Robert Penner
  • 6,148
  • 1
  • 17
  • 18
1

The original code almost works as-is in TypeScript 2.7.2, although it does have an error because values other than the 3 Kinds may be passed in.

You can say that it's possible to return undefined:

function getAnimal<K extends keyof KindMap>(key: K): undefined | KindMap[K] {
    switch (key) {
        case Kind.Dog:
            return new Dog()
        case Kind.Cat:
            return new Cat()
        case Kind.Bird:
            return new Bird()
        default:
            return undefined;
    }
}

I'd probably just use a map of the enum to the class constructor directly, as using it can easily infer the type:

const kindMap = {
    [Kind.Dog]: Dog,
    [Kind.Cat]: Cat,
    [Kind.Bird]: Bird,
}

const tweeter = new kindMap[Kind.Bird]();

If you want the function to instantiate the objects, you can also set up a parallel interface:

interface KindMap {
    [Kind.Dog]: Dog,
    [Kind.Cat]: Cat,
    [Kind.Bird]: Bird,
}

const getAnimal = <K extends Kind>(k: K): KindMap[K] => new kindMap[k]();

const meower = getAnimal(Kind.Cat);
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366