0

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...

2 Answers 2

2

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.

Sign up to request clarification or add additional context in comments.

2 Comments

This is a great answer and helped me a lot. Can you explain why reflection would be better than the simple answer. It seems a lot to do compared to the simple answer. A use case of this is something better than a switch statement.
Reflection is better just because you don't have to manage that maping constant. There will be always somebody who will create new Product class but will not update that mapping, because he/she doesn't know about it, doesn't know where it is or just forget.
1

Why not consider using a constructor?

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

const PRODUCT_MAPPING: any = {
  prod1: "Product 1",
  prod2: "Product 2",
  prod3: "Product 3"
};

class Product implements IProduct {
  constructor(public readonly type: string) {}
  name(): string {
    return PRODUCT_MAPPING[this.type];
  }
}

function getProductByType(type: string): Product {
  return new Product(type);
}

const p1 = getProductByType("prod1");
console.log(p1.name()); // Product 1

const p2 = getProductByType("prod2");
console.log(p2.name()); // Product 2

const p3 = getProductByType("prod3");
console.log(p3.name()); // Product 3

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.