3

I have many classes:

class Product {}

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

function ContentMixin<T extends Constructor<Product>>(super_class: T) {
  return class extends super_class {

    public getContents(): string {
      return 'This is content';
    }
  };
}

class Milk {

  public getTankNumber(): number {
    return 1011;
  }
}

class Sugar extends ContentMixin(Product){
  public getNumberOfCubes(): number {
    return 10000000220;
  }
}

// Many classes ...

I have product factory:

enum PRODUCTS {
  MILK = 'Milk',
  SUGAR = 'Sugar',
}

class ProductFactory {

  public static create(type: PRODUCTS) {
    return new ProductFactory.products[type]();
  }

  private static get products() {
    return {
      Milk,
      Sugar,
    }
  }
}

How do I specify the type for the create method so that I don't get errors when:

const milk  = ProductFactory.create(PRODUCTS.MILK) as Milk;

milk.getTankNumber();

Property 'getTankNumber' does not exist on type 'Milk | Sugar'.
Property 'getTankNumber' does not exist on type 'Sugar'.

Using as is not the best option.

The return value depends on the passed argument. How can I tell TypeScript that with type === Milk there will be an instance of Milk?

Playguard..

doox911
  • 391
  • 5
  • 18

1 Answers1

4

You just need to create map of overloadings and overload your create method:

class Milk {

    public getTankNumber(): number {
        return 1011;
    }
}

class Sugar {
    public getNumberOfCubes(): number {
        return 10000000220;
    }
}

enum PRODUCTS {
    MILK = 'Milk',
    SUGAR = 'Sugar',
}

interface Overloads {
    [PRODUCTS.MILK]: Milk,
    [PRODUCTS.SUGAR]: Sugar
}

class ProductFactory {
    public static create<T extends PRODUCTS>(type: T): Overloads[T]
    public static create(type: PRODUCTS) {
        return new ProductFactory.products[type]();
    }

    private static get products() {
        return {
            Milk,
            Sugar,
        }
    }
}

const milk = ProductFactory.create(PRODUCTS.MILK);

milk.getTankNumber();

Playground

Why doesn't it work without a magic line public static create(type: T): Overloads[T]

Consider this example:

class ProductFactory {
    public static create<T extends PRODUCTS>(type: T): Overloads[T] {
        const union = new ProductFactory.products[type]();
        union.getTankNumber() // #1 error
        union.getNumberOfCubes() // #2 error

        return new ProductFactory.products[type](); // #3 error
    }

    private static get products() {
        return {
            Milk,
            Sugar,
        }
    }
}

union const can be either Milk or Sugar. Hence , TS will allows to call only common props. Since they don't have any common props, you can't call any method (errors #1 and #2). Try to add tag property to each Milk and Sugar:

class Milk {
    tag = {
        milk: true
    }

    public getTankNumber(): number {
        return 1011;
    }
}

class Sugar {
    tag = {
        milk: true
    }
    public getNumberOfCubes(): number {
        return 10000000220;
    }
}

class ProductFactory {
    public static create<T extends PRODUCTS>(type: T): Overloads[T] {
        const union = new ProductFactory.products[type](); // Milk | Sugar
        const tag = union.tag.milk // OK
        union.getTankNumber() // error
        union.getNumberOfCubes() // error

        return new ProductFactory.products[type]();
    }

    private static get products() {
        return {
            Milk,
            Sugar,
        }
    }
}

Now you see that const tag is working as expected. Let's go back to our original problem:


class Milk {
    public getTankNumber(): number {
        return 1011;
    }
}

class Sugar {
    public getNumberOfCubes(): number {
        return 10000000220;
    }
}

enum PRODUCTS {
    MILK = 'Milk',
    SUGAR = 'Sugar',
}

interface Overloads {
    [PRODUCTS.MILK]: Milk,
    [PRODUCTS.SUGAR]: Sugar
}
type O = Overloads[PRODUCTS]

class ProductFactory {
    public static create<T extends PRODUCTS>(type: T): Overloads[T] {
  
        return new ProductFactory.products[type](); // error
    }

    private static get products() {
        return {
            Milk,
            Sugar,
        }
    }
}

const milk = ProductFactory.create(PRODUCTS.MILK);

milk.getTankNumber();

Error:

Type 'Milk | Sugar' is not assignable to type 'Overloads[T]'.
  Type 'Milk' is not assignable to type 'Overloads[T]'.
    Type 'Milk' is not assignable to type 'Milk & Sugar'

Because T generic parameter is in contravariant position, Milk and Sugar were intersected. Since, you can only return one of them, TS throwing an error.

From the other hand, function overloads are not so strict and you allowed to do things like that

Where did you find out that the overloads are not strict?

From my experience. See this issue

And I still don't understand why TS swears if it understands which class is returned

See my question and this talk