Repository: TypeScript
Issue: Allow non-polymorphic ("private") extension of base classes
Labels: Suggestion,Awaiting More Feedback

### 🔍 Search Terms

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

### ✅ Viability Checklist

- [X] This wouldn't be a breaking change in existing TypeScript/JavaScript code
- [X] This wouldn't change the runtime behavior of existing JavaScript code
- [X] This could be implemented without emitting different JS based on the types of the expressions
- [X] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- [X] This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- [X] This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

### ⭐ 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:

```js
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:

```ts
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`:
   ```ts
   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.