I try to use Fabric method for creating two kind of concrete products which have different config in a constructor. TS Interpreter gets me an error about config, because, as I see it tries to mix them, but the problem is they are different for each product and I'm explicitly pointing them out as - type ConfigI = ConfigProduct1 | ConfigProduct2
What Im doing wrong ?
Here is link to playground.
type ConfigProduct1 = {
arg1: string
}
type ConfigProduct2 = {
arg2: string
}
type ConfigI = ConfigProduct1 | ConfigProduct2
enum TYPE {
PRODUCT1,
PRODUCT2
}
class Creator {
static createProduct(type: TYPE, config: ConfigI) {
switch(type) {
case TYPE.PRODUCT1:
return new ConcreteProduct1(config)
case TYPE.PRODUCT2:
return new ConcreteProduct2(config)
default:
throw new Error('Wrong Product type')
}
}
}
abstract class Product {
protected config: ConfigI
get cfg() {
return this.config
}
constructor(config: ConfigI) {
this.config = config
}
abstract foo(): void
}
class ConcreteProduct1 extends Product {
foo() {
console.log(this.config.arg1) // here is mistake it can't find arg2 in config
}
}
class ConcreteProduct2 extends Product {
foo() {
console.log(this.config.arg2) // here is mistake it can't find arg1 in config
}
}
const product = Creator.createProduct(TYPE.PRODUCT1, {arg1: 'something'})
Moreover, if I try to use product as a link in an another class method Interpreter gets me an error what argument does not exists at all in product config, e.g.
// next one
class ProductUsing {
protected product: Product
constructor(product: Product) {
this.product = product
}
bar() {
console.log(this.product.cfg.arg1) // here an error - Property 'arg1' does not exist on type 'ConfigI'.
}
}
CodePudding user response:
I have fixed your code here:
type ConfigProduct1 = {
cfg1: string
}
type ConfigProduct2 = {
cfg2: string
}
abstract class Product<T extends ConfigI> {
protected config: T
constructor(config: T) {
this.config = config
}
abstract foo(): void
}
class ConcreteProduct1 extends Product<ConfigProduct1> {
foo() {
console.log(this.config.cfg1)
}
}
class ConcreteProduct2 extends Product<ConfigProduct2> {
foo() {
console.log(this.config.cfg2)
}
}
type ConfigI = ConfigProduct1 | ConfigProduct2
enum TYPE {
PRODUCT1,
PRODUCT2
}
const map = {
[TYPE.PRODUCT1]: ConcreteProduct1,
[TYPE.PRODUCT2]: ConcreteProduct2,
}
class Creator {
static createProduct<T extends keyof typeof map>(type: T, config: InstanceType<typeof map[T]> extends Product<infer U> ? U : never): InstanceType<typeof map[T]> {
return new (map[type] as any)(config);
}
}
const product = Creator.createProduct(TYPE.PRODUCT1, { cfg1: 'somethere' });
The key elements:
- You need a runtime construct in order to create the mapping between the
enum
and the classes. I've called itmap
- The return value of
createProduct
has to be casted asany
in the function implementation because the parameter types are generic. They are not resolved inside the function implementation, so TypeScript cannot match the class and its constructor parameter type. It only does on the function call and that's whyCreator.createProduct(TYPE.PRODUCT1, { cfg1: 'somethere' })
works fine.