Łukasz Zaroda
Łukasz Zaroda

Reputation: 767

Factory creating a wrapped object based on input object

I try to create a factory, that would return a wrapped object based on the input object. Return type should be dependent on the input. I kind of got it, but with a horrible type assertions in the factory method, because it seems to be impossible to implement the interface I defined.

There are finite types of Data available for an input, and finite types of Wrappers that can be used for wrapping the result. There are also finite classes of objects that can be wrapped.

Is there any possible way of implementing it in a way that would not require removing type-checking (as any) in the factory's implementation?

And one more question that would help me a lot with understanding what's going on here exactly:

Without removing type checking there is this error:

Type 'DefaultSingleWrapped<T["class"]>' is not assignable to type 'WrappersMap<T["class"]>[T["type"]]'.
  Type 'DefaultSingleWrapped<T["class"]>' is not assignable to type 'SingleWrapped<T["class"]> & ArrayWrapped<T["class"]>'.
    Type 'DefaultSingleWrapped<T["class"]>' is not assignable to type 'ArrayWrapped<T["class"]>'.
      The types returned by 'getValue()' are incompatible between these types.
        Type 'T["class"]' is not assignable to type 'T["class"][]'.(2322)

From where this SingleWrapped<T["class"]> & ArrayWrapped<T["class"]> intersection comes from? It's somehow inferred but I'm not sure what's the logic behind it.

enum DataTypes {
  DATA_TYPE1 = 'datatype1',
  DATA_TYPE2 = 'datatype2',
}

class Class1 {
  property1: boolean = true;
}

class Class2 {
  property2: boolean = false;
}

interface ServiceObjectMap {
  'class1': Class1,
  'class2': Class2,
}

type ClassUnion = ServiceObjectMap[keyof ServiceObjectMap];

interface Data<T = ClassUnion> {
  type: DataTypes,
  class: T,
}

interface DataOne<T extends ServiceObjectMap[keyof ServiceObjectMap]> extends Data<T> {
  type: DataTypes.DATA_TYPE1,
}

interface DataTwo<T extends ServiceObjectMap[keyof ServiceObjectMap]> extends Data<T> {
  type: DataTypes.DATA_TYPE2,
}

interface SingleWrapped<T> {
  getValue(): T,
}

interface ArrayWrapped<T> {
  getValue(): T[],
}

interface WrappersMap<T> {
  [DataTypes.DATA_TYPE1]: SingleWrapped<T>,
  [DataTypes.DATA_TYPE2]: ArrayWrapped<T>,
}

class DefaultSingleWrapped<T> implements SingleWrapped<T> {

  constructor(private obj: T) {}

  getValue(): T {
    return this.obj;
  }
}

interface Factory {
  createWrappedObjectFromData<T extends Data>(data: T): WrappersMap<T['class']>[T['type']];
}

class DefaultSingleWrappedFactory implements Factory {
  createWrappedObjectFromData<T extends Data<T['class']>>(data: T): WrappersMap<T['class']>[T['type']] {
    return new DefaultSingleWrapped<T['class']>(new ((data['class'] as any)['constructor'])({zz: 'bleble'})) as any;
  }
}

const factory1: Factory = new DefaultSingleWrappedFactory();
const data1: DataOne<Class1> = {
  type: DataTypes.DATA_TYPE1,
  class: Class1['prototype'],
};

const i = factory1.createWrappedObjectFromData(data1);
const value = i.getValue();
console.log(value.property1);

Playground Link

Upvotes: 0

Views: 246

Answers (1)

Cheng Dicky
Cheng Dicky

Reputation: 518

There are two as any, each of them represent a type problem.

The first one is that the interpreter do not know that data['class']['constructor'] is a constructor. In case to solve it, we can create a Constructor type to replace ['constructor'] and ['prototype'].

type Constructor<T> = new (...args: any[]) => T

interface Data<T = ClassUnion> {
    type: DataTypes,
    class: Constructor<T>,
}

createWrappedObjectFromData(data) {
    return new DefaultSingleWrapped(new data.class({zz: 'bleble'}));
}

The second one is that typescript cannot interpret WrappersMap<T['class']>[T['type']] correctly.

As you stated that your types are known and limited, I would suggest you to use function overloading.

abstract class Wrapped {
    abstract getValue(): any
}

interface SingleWrapped<T> extends Wrapped {
    getValue(): T,
}

interface ArrayWrapped<T> extends Wrapped {
    getValue(): T[],
}

interface Factory {
    createWrappedObjectFromData<T extends ClassUnion>(data: Data<T>): Wrapped;
}

class DefaultSingleWrappedFactory implements Factory {
    createWrappedObjectFromData<T extends ClassUnion>(data: DataOne<T>): SingleWrapped<T>
    createWrappedObjectFromData<T extends ClassUnion>(data: DataTwo<T>): ArrayWrapped<T>
    createWrappedObjectFromData<T extends ClassUnion>(data: Data<T>): Wrapped {
        return new DefaultSingleWrapped(new data.class({zz: "bleble"}));
    }
}

Here is a modified version of the playground.

Upvotes: 1

Related Questions