简体   繁体   中英

How to describe prototypal inheritance in Typescript?

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 (eg 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); Bf(); // 3 B.bar(); // 10

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.


Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM