3

I am currently using factory pattern in my project, below is the minimal reproducible code

class A {
    constructor(public name: string) {}
}

class B {
    constructor(public age: number, public address: string) {}
}

type FactoryMap = {
    A: A,
    B: B
}

const map = {
    A,
    B
}

function create<T extends keyof FactoryMap>(name: T, ...params: ConstructorParameters<typeof FactoryMap[T]>): FactoryMap[T] {
    switch (name) {
        case "A":
            return new A(...params);
        case "B":
            return new B(...params);
    }
}

function create2<T extends keyof typeof map>(name: T, ...params: ConstructorParameters<typeof map[T]>): typeof map[T] {
    return new map[name](...params);
}

I've tried two different functions, but neither could work.

The first one said Type 'A' is not assignable to type 'A & B', but I wonder FactoryMap[T] should be A | B.

The second one's error is Type 'A | B' is not assignable to type '{ A: typeof A; B: typeof B; }[T]', I know why but I can't infer A | B using the object map.

Does anyone has solutions on those two functions?

playground

2
  • as types exist only through a compilation phase what is the reason in create function? instead of calling create<A> just create an object with new A(...) command. Commented Nov 23, 2021 at 14:01
  • @AndrejKirejeŭ Similar to document.createElement("div" | "span" | "p"), I can easily create instances of different types. Commented Nov 23, 2021 at 15:42

1 Answer 1

2

Typescript will not be able to evaluate conditional types that contain unresolved type parameters. You could solve this with overloads:

class A {
    private name!: string
    constructor() {}
}

class B {
    private age!: number
    constructor() {}
}

const map = { A, B }

function create2<T extends keyof typeof map>(name: T, ...params: ConstructorParameters<typeof map[T]>): InstanceType<typeof map[T]>
function create2(name: keyof typeof map, ...params: ConstructorParameters<typeof map[keyof typeof map]>): InstanceType<typeof map[keyof typeof map]> {
    return new map[name](...params);
}

I added InstanceType on the return type. typeof map[keyof typeof map] will be the union typeof A | typeof B (ie either of the classes). Since we return an instance, we need to use InstanceType to get that.

The version above will not actually work if you have parameters to the constructor. This is because Typescript won't be able to follow that params and map[name] are in any way correlated. So TS sees new (A | B)(...(parmsForA | paramsForB) which is only sometimes safe (if A is paired with paramsForB this would be an error)

Now since we know that the two are actually correlated, this is a good use of a type assertion:

class A {
    constructor(public name: string) {}
}

class B {
    constructor(public age: number, public address: string) {}
}

const map = { A, B }

function create2<T extends keyof typeof map>(name: T, ...params: ConstructorParameters<typeof map[T]>): InstanceType<typeof map[T]>
function create2(name: keyof typeof map, ...params: any[]): InstanceType<typeof map[keyof typeof map]> {
    return new (map[name] as any)(...params)
}

Playground Link

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

7 Comments

Nice work! But I can't figure out the meaning of using overload, without overload, I can achieve the same effect. playground
@crazyones110 The overload is there for good client side types. create2("A", ...paramsOfA) shold return an instance of A. If the implementation signature were the only one , that would return A | B
why do you cast map[name] as any? Don't you break the type checking here?
@GuerricP My point is that in this case there isn't a version of this that will type check currently
@TitianCernicova-Dragomir In my playground, the implementation signature is the only one, TS still can correctly infer type "A" or "B" for template parameter T and the return type is A or B corresponding to inferred T
|

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.