I'm trying to create a wrapper class. I have a Base class from an imported package and want to wrap it with some logging and try/catch, in such a way that the Wrapper class is fully abstracted away from the user, and can be used as if using the Base class. What I have so far:
class Base {
name: string;
constructor(name: string) {
this.name = name;
}
first() {}
second(val: number) {}
third(vals: number[]) {}
... // a lot of methods, can't map them manually
}
class Wrapper {
bases: Base[] = [];
constructor(names: string[]) {
for (const name of names) {
this.bases.push(new Base(name));
}
}
}
Object.getOwnPropertyNames(Base.prototype).forEach((methodName) => {
if (methodName !== 'constructor') {
Wrapper.prototype[methodName] = function (...args) {
for (const base of this.bases) {
try {
// logging
return base[methodName].apply(base, args);
} catch {}
}
};
}
});
The problem with this is that I get some type warnings:
No index signature with a parameter of type 'string' was found on type 'Wrapper'No index signature with a parameter of type 'string' was found on type 'Base'
and ignoring those, the wrapper class doesn't inherit the Base type, so that if I do
const wrapper = new Wrapper(['a', 'b', 'c']);
const s = wrapper.first();
I get Property 'first' does not exist on type 'Wrapper'.
Is there a better way to ensure type safety besides overriding manually the type as unknown as Base? I could extend the Base class though I would miss some class properties, such as name in this example.
EDIT: added some more examples of method in Base class. The Base class is quite large, so there are lot of different methods, including with optional parameters, overloads, etc
Assuming we care more about the experience of those using
Wrapperand less about the TypeScript compiler being able to verify that we have implementedWrapperproperly, I would be inclined to proceed like this:First, let's determine which properties of
Basewe need to copy over intoWrapper. It looks like it's supposed to be all the methods. TypeScript can't really tell the difference between a method (on the prototype) and a function-valued property (on each instance) so I'm hoping you don't have any of the latter. We can use a key-remapped mapped type to filter the properties of an object to just the function properties:Then, we can merge
FuncsOf<Base>into theWrapperinstance type:This tells TypeScript that a
Wrapperinstance also is assignable toFuncsOf<Base>, without requiring or caring that it's actually implemented anywhere. This is a bit dangerous, since if you forgot to write your loop overBase.prototypeentries, or if you wrote it incorrectly, the compiler won't know or care. But I'm not worried about that here because as I said at the top, we're focusing on the users ofWrapperand not the implementer. We'll just have to be careful with the implementation.And since that's the case, I'm just going to use the
anytype and type assertions to sidestep any compiler errors inside this loop:There are ways to begin to try to write this so the compiler checks the loop more closely, but frankly they are more trouble than they're worth. (If you're interested in going down the rabbit hole, you can look at microsoft/TypeScript#47109 to get a flavor of the kinds of typing gymnastics involved.)
Anyway, let's try it out:
Looks good!
Playground link to code