Issue
I have a JavaScript project that I am refactoring into TypeScript. However, I've encountered a TypeScript error (TS2339) that I cannot resolve.
I have a class
export const devtoolsBackgroundScriptClient = new class
{
constructor()
{
return this.asProxy();
}
// some methods
asProxy()
{
return new Proxy(this, {
get: (e, t) => {
return void 0 !== e[t] ? e[t] : e.callBackgroundScript.bind(e, t);
}
});
}
callBackgroundScript(method, ...params)
{
// some code
}
}
I call this class, like a
devtoolsBackgroundScriptClient.submitSurvey(surveyData);
Method submitSurvey doesnt exist in class, that's why called callBackgroundScript. It works for javascript, but I have typescript error here:
devtoolsBackgroundScriptClient.submitSurvey(surveyData);
Property 'submitSurvey' does not exist on type 'devtoolsBackgroundScriptClient'
How I can resolve ts warning?
Solution
TypeScript unfortunately currently has no way to model that a Proxy
does anything to the type of its target. So asProxy()
return type is just the this
type as if the proxy weren't there. This is a known issue, reported at microsoft/TypeScript#20846. It's classified as a bug but I'd think of it more as a missing feature. Either way it's not part of the language so you'll need to work around it.
Furthermore, TypeScript currently has no way to model that a class constructor might return
something different from the type of the class itself. Even if asProxy()
returned something more specific than the anonymous this
type, the class constructor wouldn't notice it. There is a longstanding feature request for the ability to annotate a class constructor's return type at microsoft/TypeScript#27594. Again, not part of the language, so you'll need to work around it.
The best you'll be able to do is come up with a type that works for you and then either assert that the value is of that type, or use some other type-loosening scheme like the intentionally unsound any
type. Here's one possible approach:
First, it's a nightmare to try to keep track of your class's non-augmented type when everything is anonymous. I'm going to just use a class declaration instead of a class expression. It's not necessary but it's a lot easier since it gives us a type name to work with:
class Foo {
constructor() {
}
a() {
console.log("called a")
}
callBackgroundScript(method: PropertyKey, ...params: any) {
console.log(method, params)
}
}
I added an example method in there also. Note that I didn't bother to put the Proxy
inside the class. You were just using the class once, so it's more straightforward to put the class instance inside the proxy instead. I don't want to think about what happens if someone calls devtoolsBackgroundScriptClient.asProxy().asProxy()
anyway; it's better to just avoid that possibility.
Okay, here we go:
const devtoolsBackgroundScriptClient = new Proxy(new Foo(), {
get: (e: any, t) => {
return void 0 !== e[t] ? e[t] : e.callBackgroundScript.bind(e, t);
}
}) as Foo & { [k: string]: (...params: any) => void }
The important piece of that is that devtoolsBackgroundScriptClient
is of type Foo & { [k: string]: (...params: any) => void }
. That intersection is saying that it's both a Foo
and a { [k: string]: (...params: any) => void }
. The latter type has a string index signature and says that at every string key there is a method that takes any arguments.
Note that this type isn't quite accurate. Conceptually you'd want to say "every string key except for those already of Foo
". Unfortunately, there's no way to accurately represent such a type in TypeScript. This is another missing feature: microsoft/TypeScript#17867. Until and unless there is a way to do that, you'll (guess what) have to work around it. See How to define Typescript type as a dictionary of strings but with one numeric "id" property for various workarounds. The intersection type here works pretty well in practice as long as you're not trying to create a value of that type without error, and the type assertion as
already handles that for us.
Okay, let's try it out:
const surveyData = { a: 1 }
devtoolsBackgroundScriptClient.submitSurvey(surveyData); // okay, "submitSurvey" [{a: 1}]
devtoolsBackgroundScriptClient.a(); // okay, "called a"
devtoolsBackgroundScriptClient.a(surveyData); // compiler error (still emits "called a")
Looks good. The compiler knows that the a
method exists and does not allow arguments, while submitSurvey
is allowed and treated like a method that accepts any arguments.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.