Search code examples
typescriptgenericsfactoryunion-types

Best Approach for narrowing factory return type without passing in class


I am trying to find the best approach for narrowing the return type of a factory without passing in the class itself. Below is an example using conditional types, which is the best approach I've found so far. Is there a cleaner way to narrow the return type?

Setup

class Lion {
  run() {
    // Do something
  }
}

class Fish {
  swim() {
    // Do something
  }
}

class Snake {
  slither() {
    // Do something
  }
}

enum AnimalType {
  LION = 'lion',
  FISH = 'fish',
  SNAKE = 'snake',
}

This example returns a union type, which I don't want

class AnimalFactory {
  public static create(type: AnimalType) {
    switch (type) {
      case AnimalType.LION:
        return new Lion();
      case AnimalType.FISH:
        return new Fish();
      case AnimalType.SNAKE:
        return new Snake();
      default:
        throw new Error('Invalid Type');
    }
  }
}

const fish = AnimalFactory.create(AnimalType.FISH); // Type: Lion | Fish | Snake :(

This example returns the correct type, but seems like a lot of overhead

type Animal<T> = T extends AnimalType.LION
  ? Lion
  : T extends AnimalType.FISH
  ? Fish
  : T extends AnimalType.SNAKE
  ? Snake
  : unknown;

class AnimalFactory {
  public static create<T extends AnimalType>(type: T): Animal<T> {
    switch (type) {
      case AnimalType.LION:
        return new Lion() as Animal<T>;
      case AnimalType.FISH:
        return new Fish() as Animal<T>;
      case AnimalType.SNAKE:
        return new Snake() as Animal<T>;
      default:
        throw new Error('Invalid Type');
    }
  }
}

const fish = AnimalFactory.create(AnimalType.FISH); // Type: Fish :)

Is there a cleaner way to narrow the factory return type using only the AnimalType argument?


Solution

  • You need a type that describes which class type is returned for which enum value:

    interface AnimalFactoryReturnType {
      [AnimalType.LION]: Lion;
      [AnimalType.FISH]: Fish;
      [AnimalType.SNAKE]: Snake;
    }
    

    Then you can use keyof to select it out:

    class AnimalFactory {
      public static create<T extends keyof AnimalFactoryReturnType>(type: T): AnimalFactoryReturnType[T] {
        switch (type) {
          case AnimalType.LION:
            return new Lion();
          case AnimalType.FISH:
            return new Fish();
          case AnimalType.SNAKE:
            return new Snake();
          default:
            throw new Error('Invalid Type');
        }
      }
    }
    
    const fish = AnimalFactory.create(AnimalType.FISH); // Type: Fish :)