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