I am trying to write a function that accepts an API interface (I've created a sample here), and wraps a Proxy
around it, so that any calls to the that API's methods get intercepted, and I can do some logging, custom error handling etc. I am having a terrible time with the types. This is similar to another question I have asked (Writing wrapper to third party class methods in TS), but uses a completely different approach than that one, based on some feedback I got.
Currently I am getting
Element implicitly has an 'any' type because expression of type 'string | symbol' can't be used to index type 'API'. No index signature with a parameter of type 'string' was found on type 'API'.
which makes sense given that sayHello
is not strictly a string
as far as typescript is concerned, but I do not know the best way to be able to get methods on this class without uses the property accessor notation.
class API {
sayHello(name: string) {
console.log(“hello “ name)
return name
}
}
export default <T extends API>(
api: T,
) =>
new Proxy(api, {
get(target, prop) {
if (typeof target[prop] !== "function") { // type error here with "prop"
return target[prop]; // and here
}
return async (...args: Parameters<typeof target[prop]>) => {
try {
const res = await target[prop](...args); // and here
// do stuff
return res
} catch (e) {
// do other stuff
}
};
},
});
Is this possible in TS?
CodePudding user response:
You can try add index signature to your class:
class API {
/* -- index signature -- */
[index:string|symbol]: any;
sayHello(name: string) {
console.log("hello" name)
}
}
export default <T extends API>(
api: T,
) =>
new Proxy(api, {
get(target, prop) {
if (typeof target[prop] !== "function") {
return target[prop];
}
/* -- had to store it in a variable -- */
const data = target[prop];
return async (...args: Parameters<typeof data>) => {
try {
const res = await target[prop](...args);
// do stuff
return res.data;
} catch (e) {
// do other stuff
}
};
},
});
CodePudding user response:
TypeScript doesn't currently model the situation when a Proxy
differs in type from its target object. There's a longstanding open feature request for this at microsoft/TypeScript#20846, and I don't know if or when the situation will change.
For now if you want to do this you'll need to manually describe the expected return type of your proxy function, and use type assertions liberally inside the implementation in order to suppress any errors. This means you'll need to verify that your function is type safe yourself; the compiler won't be able to help much.
Here's one possible approach:
const prox = <T extends API>(api: T) => new Proxy(api, {
get(target: any, prop: string) {
if (typeof target[prop] !== "function") {
return target[prop];
}
return async (...args: any) => {
try {
const res = await target[prop](...args);
return res;
} catch (e) { }
};
},
}) as any as { [K in keyof T]: AsyncifyFunction<T[K]> };
type AsyncifyFunction<T> = T extends (...args: infer A) => infer R ?
(...args: A) => Promise<Awaited<R>> : T;
The idea is that prox(api)
returns a version of api
where every non-function property is the same, but every function property has been changed to an async version that returns a Promise
. So if api
is of type T
, then prox(api)
is of mapped type { [K in keyof T]: AsyncifyFunction<T[K]> }
, where AsyncifyFunction<T>
is a conditional utility type that represents the transformation of each property type. If X
is a function type with argument tuple A
and return type R
, then AsyncifyFunction<X>
is (...args: A) => Promise<Awaited<R>>
, using the Awaited<T>
utility type to deal with any nested promises (we don't want a Promise<Promise<number>>
to come out of this, for example).
Okay, let's test it:
class Foo extends API {
str = "abc";
double(x: number) { return x * 2 };
promise() { return Promise.resolve(10) };
}
const y = prox(new Foo());
/* const y: {
str: string;
double: (x: number) => Promise<number>;
promise: () => Promise<number>;
sayHello: (name: string) => Promise<void>;
} */
So, according to the compiler, y
has a string
-valued str
property, and the rest of its properties are asynchronous methods that return promises. Notice that the promise()
method returns Promise<number>
and not Promise<Promise<number>>
.
Let's make sure it works as expected:
console.log(y.str.toUpperCase()) // "ABC"
y.sayHello("abc") // "helloabc"
y.double(123).then(s => console.log(s.toFixed(2))) // "246.00"
y.promise().then(s => console.log(s.toFixed(2))) // "10.00"
Looks good!