microsoft / microsoft/TypeScript

Allow non-polymorphic ("private") extension of base classes

オープン
#57,979 コメント 5 件 リアクション 1 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

Awaiting More Feedback Suggestion
主要言語
Go
スター
111k
フォーク
14.4k
平均マージ
1日 19時間
マージ済み PR(30日)
117

説明

🔍 Search Terms

private non-polymorphic non-assignable inheritance extension subclass change method signature of base class

✅ Viability Checklist
⭐ Suggestion

I would like to write an ES6 class that does not inherit the typing of its base class (and thus doesn't expose any properties or methods defined in the base class, unless they are overridden in the subclass). In C++ I would call this "private inheritance", but in short: I would like to be able to use the implementation of a class (by using the super keyword in a method) without declaring my instances to be assignment-compatible.

📃 Motivating Example

To the best of my knowledge, there are currently no ways to express this in TypeScript. In pure JavaScript, I can write the following:

export class MyTuple extends Array {
  constructor(...args) {
    super();
    this.push(...args);
    Object.freeze(this);
  }
}

Instances of the MyTuple class, despite extending the Array prototype, don't conform to the Array contract, as all of the mutation methods will throw a TypeError. Even worse, direct array element assignment will silently fail, as assignment to a read-only property is simply ignored. However, the only way to get TypeScript to output this JS code is to use a TypeScript class declaration, and these always propagate the base class typings.

In this particular case, the class is a better match for the ReadonlyArray interface, so ideally I could write the following in TypeScript:

export class MyTuple<T> extends private Array<T> implements ReadonlyArray<T> {
  constructor(...args: readonly T[]) {
    super();
    this.push(...args);
    Object.freeze(this);
  }
}
💻 Use Cases
  1. What do you want to use this for?
    In my current project, I'm writing a variable-dimensionality geometry library in which the Point class is a specialization of Array<number>. Point is a generic class with the signature Point<Dims extends number>, so a 2D point is a Point<2>, a 3D point is a Point<3>, etc. Using Array as the base class provides a number of advantages, like being able to use .map() in order to create a new Point with the same dimensionality, based on the existing. However, most of the Array functionality shouldn't be exposed to consumers; in particular, anything that modifies the length of the array or creates an array of different length (push, slice, etc) would cause improper typing.
  2. What shortcomings exist with current approaches?
    Well, first off, the return type of .map() is incorrect because of #10886; it and all the other copy-instantiating methods should return a Point but instead return number[]. I can't use declare to override the method signature directly because of #38008 and #48290, and TS won't allow me to override it as a property because the types don't match.
  3. What workarounds are you using in the meantime?
    I'm removing the typing entirely by casting Array to a dummy constructor of Object:
    export class Point<Dims extends number = 2> extends (Array as new () => Object) implements PointLike<Dims> {
    
    Previously I'd been casting it to a constructor of readonly number[], but that didn't work because, as mentioned above, I need to declare map() as a method that returns a Point, and the existing number[] return signature can't be cast to a Point derived from readonly number[]. So now I'm casting it to Object, which comes with a fairly serious drawback: I can no longer use the super keyword in my methods, because TS now thinks that super is type Object, and I can't even correct it - the super keyword can't have a type assertion applied to it, as mentioned in #41034.

Now I go to write a lot of type assertions and // @ts-expect-error comments, because so far as I can tell, there is no other way around this.

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

動機となる MyTuple と Point の例から始め、関連する issue #10886、#38008、#48290、#41034 を確認してください。生成される JavaScript を変更せずに、private または非ポリモーフィックな基底型指定を行えるかどうかを判断してください。例が型チェックを通過し、代入不可能性と使用可能な super 呼び出しが維持されれば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
javascript, typescript
領域
compilers
issue の種類
機能追加
難易度
5/5
見積もり時間
1週間以上
活発さ
停滞
明瞭さ
おおむね明確
初心者へのやさしさ
25/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。