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 Wrapper
and less about the TypeScript compiler being able to verify that we have implemented Wrapper
properly, I would be inclined to proceed like this:
First, let's determine which properties of Base
we need to copy over into Wrapper
. 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:
type FuncsOf<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K]
}
Then, we can merge FuncsOf<Base>
into the Wrapper
instance type:
interface Wrapper extends FuncsOf<Base> { }
This tells TypeScript that a Wrapper
instance also is assignable to FuncsOf<Base>
, without requiring or caring that it's actually implemented anywhere. This is a bit dangerous, since if you forgot to write your loop over Base.prototype
entries, 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 of Wrapper
and 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 any
type and type assertions to sidestep any compiler errors inside this loop:
Object.getOwnPropertyNames(Base.prototype).forEach((methodName) => {
if (methodName !== 'constructor') {
(Wrapper.prototype as any)[methodName] = function (...args: any) {
for (const base of this.bases) {
try {
// logging
return base[methodName].apply(base, args);
} catch { }
}
};
}
});
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:
const wrapper = new Wrapper(['a', 'b', 'c']);
const s = wrapper.first(); // okay
wrapper.second(2); // okay
wrapper.third([1, 2, 3]); // okay
Looks good!