Search code examples
javascripttypescriptclassreflectioninstantiation

Dynamically generate and return a class object


In my database I have a product_type ('prod1' | 'prod2' | 'prod3').

I would like to generate a class object based on its type.

Here's my TypeScript code:

interface Product {
  readonly type: string;
  name(): string;
}

class Product1 implements Product {
  readonly type: string = 'prod1';
  name: () => 'Product 1';
}
class Product2 implements Product {
  readonly type: string = 'prod2';
  name: () => 'Product 2';
}
class Product3 implements Product {
  readonly type: string = 'prod3';
  name: () => 'Product 3';
}

function getProductByType(type: string) {
  // TODO: return Product1, Product2 or Product3
  if (type == 'prod1') return new Product1();
  else if (type == 'prod2') return new Product2();
  else return new Product3();
}

The problem is in the getProductByType function. Is there an approach to return a concrete Product class based on the passed type without having multiple if-else statements?

This sounds like a good case for a factory strategy pattern but I can't figure out how to correctly implement it here...


Solution

  • There are several possible solutions, it depends how much "automated" the solution should be.

    1) Simple

    Using mapping, similar to AdityaParab's answer.

    
    class Product1 {}
    class Product2 {}
    class Product3 {}
    
    // Mapping from type name to class object.
    const ProductMap = {
      prod1: Product1,
      prod2: Product2,
      prod3: Product3
    };
    
    function getProductByType(type) {
        const Product = ProductMap[type];
        if (!Product) throw new Error(`Unknown ProductType '${type}'.`);
        return new Product(type);
    }
    
    console.log(getProductByType("prod1"));
    

    2) Real Reflection (requires TypeScript transformer)

    StackBlitz demo here.

    Check out tst-reflection GitHub repo.

    Decorator used to mark Product classes.

    /**
     * Decorator used to mark Product classes.
     * @reflect - required JSDoc property. Means all decorated types can be used in reflection.
     * @param productType
     */
    export function ForProductType(productType: string) {
      return (ctor: Function) => {};
    }
    

    Decorated Product class.

    @ForProductType('prod1')
    export class Product1 implements Product {
      readonly type: string = 'prod1';
    
      get name() {
        return 'Product 1';
      }
    }
    

    getProductByType function with a little bit of reflection.

    // Some reflection job.. Find all types decorated by the ForProductType decorator and create map of those types.
    const entries = Type.getTypes()
      .map<[string, Type]>((type) => [
        type
          .getDecorators()
          .find((decorator) => decorator.name == 'ForProductType')
          ?.getArguments()[0],
        type,
      ])
      .filter(([typeName, type]) => !!typeName);
    const ProductTypeMap = new Map<string, Type>(entries);
    
    function getProductByType(type: string): Promise<Product> {
      const ProductType: Type = ProductTypeMap.get(type);
    
      if (!ProductType) {
        throw new Error(`Unknown ProductType '${type}'.`);
      }
    
      return ProductType.getCtor().then((Product) => new Product());
    }
    

    Usage

    getProductByType('prod1').then((product) => console.log(product, product.name));
    

    It returns Promise cuz it does dynamic imports of the Product classes.