Generic factory based on interface TypeScript

Viewed 2801

I want to implement generic method which will return concrete type based on provided interface. Is this even possible in TypeScript?

Here is my pseudo example:

interface IDog {
  canRun: boolean;
}

interface IBird {
  canFly: boolean;
}

class Dog implements IDog {
  canRun: boolean;
}

class Bird implements IBird {
  canFly: boolean;
}

function createInstance<T>(): T {
  const key =  typeof T;

  switch (key) {
    case IBird:
      return new Bird();
     return 
      case IDog:
      return new Dog();
    default:
      break;
  }
}

// concrete is Dog
const concrete = createInstance<IDog>();

createInstance() method is a raw example of what I am trying to achieve, it will not compile!

I would like to provide interface type to createInstance() method and implement some logic which will create concrete type for provided interface.

Is it possible in TypeScript?

4 Answers

Interfaces are not accessible in runtime, but you can add type checking using String Literal Types:

interface IDog {
    canRun: boolean;
}

interface IBird {
    canFly: boolean;
}

class Dog implements IDog {
    canRun: boolean;
}

class Bird implements IBird {
    canFly: boolean;
}

function createInstance(type: 'bird'): Bird;
function createInstance(type: 'dog'): Dog;
function createInstance(type: string): any {
  switch (type) {
    case 'bird':
        return new Bird();
    case 'dog':
        return new Dog();
  }
}

// concrete is Dog
const concrete = createInstance('dog');

Also I think the factory return type should be interface:

function createInstance(type: 'bird'): IBird;
function createInstance(type: 'dog'): IDog;

Update:

The alternative is to store references to classes:

interface IInterfaces {
    dog: IDog;
    bird: IBird;
}

type IFactory<T> = {
    [P in keyof T]: new() => T[P];
}

let factory: IFactory<IInterfaces> = {
    dog: Dog,
    bird: Bird
}

let dog = new factory.dog();

You will need to use overloads:

function createInstance(key: 'dog'): Dog;
function createInstance(key: 'bird'): Bird;
function createInstance(key: 'dog' | 'bird') {
  // implement
}

Then pass the key as a parameter

const dog = createInstance('dog'); // typeof dog is Dog

Of course you can do a factory method in TypeScript. One way to do it:

interface IAnimal {

}

interface IDog extends IAnimal {
    canRun: boolean;
}

interface IBird extends IAnimal {
    canFly: boolean;
}

class Dog implements IDog {
    canRun: boolean;
}

class Bird implements IBird {
    canFly: boolean;
}

class AnimalFactory {
    public static create(animalType: string): IAnimal {
        switch(animalType) {
            case 'dog':
                return new Dog();
            case 'bird':
                return new Bird();
            default:
                throw new Error('Wrong animal type.');
        }
    }
}

const dog: IDog = AnimalFactory.create('dog');
const bird: IBird = AnimalFactory.create('bird');

Note that the factory hides actual class names / implementations and operates through interfaces. You can go even further if you'd like to and use builder pattern to return your animals compatible with IAnimal interface, like

case 'dog':
    const dog: IDog = new DogBuilder()
        .setColor('white')
        .setWeight(30)
        .build() // whatever, you get the idea
    return dog;

The problem is that TS interfaces are not available at runtime, so you can't use JS to create the interfaces. BUT! they exist on the code, so you can read the file and use solutions that use the file system to solve this.

Recently I found this: https://github.com/google/intermock, which seems really useful. I haven't tried it yet, but It's the only solution for this problem that I have ever found.

node build/src/cli/index.js --files ./example-file.ts --interfaces "Admin"

You select the files where the interfaces are located and the interfaces that you want to create an object from.

You could use something like husky to run a command before testing that generates the mocks before the actual tests

Related