microsoft / microsoft/TypeScript

Allow "instance" private and protected class members that don't affect assignability

Open
#52,119 1 comment 5 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Awaiting More Feedback Suggestion
Dominant language
Go
Stars
111k
Forks
14.4k
Avg merge
1d 19h
Merged PRs (30d)
117

Description

Suggestion

Many classes use private and protected members for internal use only - that is, they always access them via this.field and never on other object with other.field.

For these classes it would be very useful to retain structural typing, instead of the effectively nominal typing you get with private and protected members currently.

This would be especially useful for avoiding compilation errors when programs have multiple copies of the same package installed (for the usual npm reasons) with classes that are actually compatible with each other, but aren't assignable according to TypeScript.

Could we add an instance modifier to class members that restricts the fields to only being access with this.?

Example:

class A {
  instance private _x = 0;
  get x() { return this._x; }
}

class B {
  instance private _x = 0;
  get x() { return this._x; }
}

const f = (a: A) => {}

// Should not be an error
f(new B());

🔍 Search Terms

instance private assignable

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Add an instance modifier that enforces that the member is only accessed with this.:

class A {
  instance private _x = 0;

  get x() {
    // fine
    return this._x;
  }

  getOtherX(o: A) {
    // error
    return o._x;
  }
}

📃 Motivating Example

Besides the common case using a class type as an interface, it's far too easy to end up in situations where users have multiple copies of a package and pass objects from one copy to another:

node_modules/foo/index.ts:

export class A {
  instance private _x = 0;
  get x() { return this._x; }
}
export const getX = (a: A) => a.x;

node_modules/bar/node_modules/foo/index.ts:

export class A {
  instance private _x = 0;
  get x() { return this._x; }
}
export const getX = (a: A) => a.x;

node_modules/bar/index.ts:

export {getX} from 'foo';

src/app.ts:

import {A} from 'foo';
import {getX} from 'bar';

// This causes a compile error but no runtime error:
getX(new A());

💻 Use Cases

Workarounds

There are some workarounds, but they are finicky and don't work completely.

Interface<T> helper:

You can define a helper to get just the instance interface of a class like:

type Interface<T> = {
  [K in keyof T]: T[K];
}

But using it isn't trivial:

export class AImpl {
  private _x = 0;
  get x() { return this._x; }
}
export const getX = (a: A) => a.x;

export type A = Interface<AImpl>;
export const A = AImpl;

Unfortunately, the exported type A doesn't fully represent the public interface of class AImpl. It doesn't have the static interface and it doesn't have the protected members which may be necessary for subclasses (and can still follow the rule that they are accessed only on the this instance).

/** @internal */

You can mark private members as /** @internal */ to exclude them from the published public types. This does not work for protected members though.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No source file, test, or compiler entry point is named. Start by reviewing the proposal's private and protected member assignability examples and the TypeScript design goals, then determine the affected type-checking rules and tests needed to define the new modifier's behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.