Skip to content

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

@justinfagnani

Description

@justinfagnani

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions