Reputation: 9257
The code below is valid Javascript. Typescript, reasonably, doesn't understand B's this.a
references and calls them out. Is there some type-fu that can be applied to B to explain the run time circumstances to Typescript and get the benefit of type-safety (e.g. an error if trying to access this.c
)?
const A = {
a: 1,
bar() {
this.a = 10;
},
};
const B = {
b: 2,
f() {
console.log(this.a + this.b);
},
bar() {
super.bar();
console.log(this.a);
},
};
Reflect.setPrototypeOf(B, A);
B.f(); // 3
B.bar(); // 10
Upvotes: 3
Views: 280
Reputation: 327964
I think the only way you're going to get something like this to work (without having to explicitly write out redundant type annotation information everywhere) is if you refactor the assignment of B
and setting of its prototype into a single function call like this:
function setPrototype<T extends object, U extends object>(
object: T & ThisType<T & U>,
proto: U
) {
Reflect.setPrototypeOf(object, proto);
return object as T & U;
}
If you call setPrototype({...}, proto)
, where {...}
is some object literal of generic type T
, and where proto
is some other object of generic type U
: the compiler will set the object literal's prototype to proto
and return it.
In this call, you will see that the object literal will get a contextual type for this
which is an intersection of both T
and U
. This happens via the magic ThisType<T>
utility type. When I say "magic" here, I mean that, unlike most other utility types, you could not define an equivalent type yourself. The compiler literally gives special contextual powers to ThisType<T>
; see microsoft/TypeScript#14141 for a description of how this works.
Finally, the returned object type will also have that T & U
intersection typing, so you can access both its own-properties and the properties inherited from the prototype.
Let's try it out:
const A = {
a: 1,
bar() {
this.a = 10;
},
};
const B = setPrototype({
b: 2,
f() {
console.log(this.a + this.b);
},
bar() {
super.bar();
console.log(this.a);
},
}, A);
B.f(); // 3
B.bar(); // 10
Looks good! There are now no errors, and the compiler infers a contextual type for this.a
and this.b
inside the implementation of f()
.
There are caveats and edge cases. The intersection type T & U
might not be completely accurate, but it's as close as I can easily represent.
The bigger caveat here is that there doesn't seem to be a way to handle super
in a type-safe way. Without something like a super
parameter annotation (as asked for in microsoft/TypeScript#42327), there's no way to do it manually. And even if you could do it manually, it still wouldn't get you to the automatic contextual typing of ThisType<T>
... you'd need a new magical type alias like SuperType<T>
also. So for now, this seems to be beyond TypeScript's abilities.
Personally, I'd suggest moving away from this kind of manual prototype juggling in favor of class
-based inheritance, since there's been much more work put into TypeScript to support ES2015+ inheritance than there has been to support ES5-. But you presumably have your reasons for doing it this way; hopefully the above code will get you closer to having useful type safety.
Upvotes: 3