Typescript: Wrap function return type of generic class

Viewed 169

I am not being able to wrap my head around this issue in a generic way with Typescript, I would appreciate any help!

  • A Factory should deploy a Contract
  • A CustomFactory is a Factory and should deploy a CustomContract (that is a Contract)
  • A MockFactory should be a wrapper of all this logic

Something like this would be the goal (semi pseudocode)

interface MockFactory<F extends Factory> extends F {
    deploy: (...args: Parameters<F.prototype.deploy>) => MockContract<F.prototype.deploy.returnValue>
}

In order to illustrate the issue even better I created a Playground where you can see the errors

2 Answers

Just solved by using ReturnType and Parameters, as well needed to transform the interface into a type:

interface Contract {}

interface Factory {
    deploy: (...args: any[]) => Contract;
}

class CustomContract implements Contract {}

class CustomFactory implements Factory {
    deploy(x: number, y: number): CustomContract {
        return {};
    }
}

type MockContract<C extends Contract> = Contract & C & {
    mockProperty: number;
}

type MockFactory<F extends Factory> = F & {
    // deploy should have the parameters of the function deploy inside F (in this case CustomFactory)
    // deploy should return a MockContract of the return type of the function deploy inside F (MockContract<CustomContract> in a generic way)
    deploy: (...args: Parameters<F['deploy']>) => MockContract<ReturnType<F['deploy']>>
}

const example: MockFactory<CustomFactory> = {} as any;
example.deploy(1, 2);

Updated Playground

You could go with (abstract) classes, but interfaces are possible too. In that case, making the factory itself generic seems like the wrong move. Make the contract type generic instead. After a bit of playing around, I was able to puzzle this together:

interface Contract { }

interface Factory<A extends any[], C extends Contract> {
    deploy: (...args: A) => C;
}

// Some helper types
type FactoryArgs<F extends Factory<any, any>> = F extends Factory<infer A, any> ? A : never;
type FactoryContractType<F extends Factory<any, any>> = F extends Factory<any, infer C> ? C : never;

interface FactoryForClass<C extends new (...args: any) => Contract> {
    //deploy: C extends new (...args: infer A) => infer T ? Factory<A, T>['deploy'] : never;
    deploy: Factory<ConstructorParameters<C>, InstanceType<C>>['deploy'];
}

class CustomContract implements Contract {
    constructor(a: number, b: number) { }
}

class CustomFactory implements FactoryForClass<typeof CustomContract> {
    deploy(x: number, y: number): CustomContract {
        return new CustomContract(x, y);
    }
}

type MockContract<C extends Contract> = Contract & C & {
    mockProperty: number;
}

type MockFactory<F extends Factory<any, any>> = F & {
    deploy: (...args: FactoryArgs<F>) => MockContract<FactoryContractType<F>>;
}

const mockFactory: MockFactory<CustomFactory> = {
    deploy(a: number, b: number) {
        const customContract = new CustomContract(a, b);
        const result: CustomContract & MockContract<CustomContract> = customContract as any;
        result.mockProperty = 123;
        return result;
    }
};
Related