crazyones110
crazyones110

Reputation: 397

Typescript factory pattern with parameters

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

Upvotes: 3

Views: 2106

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250346

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

Upvotes: 2

Related Questions