I need help ensuring typesafety across the following generic abstract class, that uses a proxy:
proxy.ts
function authHandler<
F extends Families,
T extends controllerAbstract<F>
>(): any {
return {
get: function (target: T, prop: keyof T): any {
return (...args: any[]) => {
target.authorize((controllerClient: Client<F>) => {
return target[prop](controllerClient, args);
});
};
},
};
}
controllerAbstract.ts
abstract class controllerAbstract<F extends Families> {
controllerFamily: F;
constructor(controllerFamily: F) {
this.controllerFamily = controllerFamily;
return new Proxy(this, authHandler<F, typeof this>());
}
abstract build_client(): Client<F>;
authorize(consume_client: (client: Client<F>) => typeof this[keyof typeof this]) {
consume_client(this.build_client());
}
}
concrete.ts
const controllerFamilyName = 'FamilyA';
type ControllerFamliy = typeof controllerFamilyName;
class FamilyAController extends controllerAbstract<ControllerFamliy> {
constructor() {
super(controllerFamilyName);
}
build_client(): Client<ControllerFamliy> {
// build a client of type "FamilyA"
const client = { id: 'A' } as Client<ControllerFamliy>;
return client;
}
// proxy "authorize" provides client object
useClient(client: any, args: any[]) {
// i want client to implicitly be of type "FamilyAClient_t" / Client<"FamilyA">
// currently just using "any" as placeholder
}
}
types.ts
type FamilyAClient_t = { id: 'A' };
type FamilyBClient_t = { id: 'B' };
interface ClientTable {
FamilyA: FamilyAClient_t;
FamilyB: FamilyBClient_t;
}
type Families = keyof ClientTable;
type Client<F extends Families> = ClientTable[F];
N.B where i say "concrete" class, i mean the contents of concrete.ts, i.e. that being a class extending the abstract class, with FamilyAController
just being an example of this.
My goal is to make the client
parameter of the useClient
method in FamilyAController of concrete.ts to reflect the correct type based on the concrete class's controllerFamilyName
const, such that i can call client.id, and i get the correct autocomplete of "A" if controllerFamilyName == 'FamilyA'
, and "B" if controllerFamilyName == 'FamilyB'
.
I have changed the explicit types in the proxy to try and get the types to be recognized as i need, based on the solution here. This is where i think the type safety breaks down in my implementation.
However, still this is erroneous, as:
- the proxy
target[prop](...)
line is giving an error of "this expression is not callable", astarget[prop]
is of type unknown. This is because not every prop of target is indeed callable, but some are, and i want to be able to restrict the type of target to this subset. - my attempt at defining a return type for the
consume_client
function argument of the abstract classauthorize
method:authorize(consume_client: (client: Client<F>) => typeof this[keyof typeof this])
. is throwing error. The intention is for it to return the type any method return type defined in concrete. For example, if another method existed on the concrete class, then the consume client return type should be able to automatically handle this new method return type. Hopefully you can see what my intention was here - to effectively "see through" the proxy layer to maintain the same return type that would exist based on any callable method of the concrete class.
Another trialed method was to define another abstract class containing just the abstract methods which are consistent with target[props](controllerClient)
signature, and use the keyof
this class as the props type, to fix thefirst error. This was successful, but i am still unsure how to reach my final goal.
Essentially what i want is, in concrete.ts , to be able to write useClient(client)
and the client type be implicitly Client<ControllerFamily>
so i don't have to write useClient(client: Client<ControllerFamily>)
for each method of each concrete class variation. Is this possible?
Hopefully this helps to clarify my objective with the code, and the current state of my progress with it.