Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a visibility mechanism similar to friend or InternalsVisibleTo #35554

Open
4 of 5 tasks
trusktr opened this issue Dec 6, 2019 · 12 comments
Open
4 of 5 tasks

Add a visibility mechanism similar to friend or InternalsVisibleTo #35554

trusktr opened this issue Dec 6, 2019 · 12 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@trusktr
Copy link
Contributor

trusktr commented Dec 6, 2019

Suggestion

A way to allow certain other modules to access properties or methods of a class.

Use Cases

Sometimes, we want other modules (other code) to have access to certain properties or methods, but otherwise the end user not to have access to the properties/methods. This is labeled as "package protected" or similar terms in other languages.

Examples

This is how it could work, with some sort of new non-runtime syntax:

import SomeClass from './SomeClass'

export class Foo {
  visible in SomeClass
  private foo: number = 123
}

or

import SomeClass from './SomeClass'

export class Foo {
  private foo visible in SomeClass: number = 123
}

or maybe even as a special decorator, globally-declared, virtual decorator

import SomeClass from './SomeClass'

export class Foo {
  @visibleIn(SomeClass)
  private foo: number = 123
}

SomeClass would then be able to access the private property/method:

import {Foo} from './Foo'

export class SomeClass {
  doSomethingWithFoo(o: Foo) {
    console.log(o.foo) // OK, no type error.
  }
}

The code inside of SomeClass could be allowed to access the private properties. This is great at design time when the author controls the source for both sides, but still wants to hide private parts from the end user on the outside of the APIs.

This allows for patterns like object managers being able to control certain aspects of the objects they manage (for example a renderer in a game engine that manages how objects in a scene render, and the renderable properties that the engine manager accesses, which are private to the end user) are composed from reactions to changes in the objects' public APIs by end users).

At the moment, the only way to allow objects to access properties of other objects is to make them public, but this does not allow for us to hide (at least in the type system) certain implementation details of a cross-class or cross-module whole system from the end user.

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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
    • I'm not sure about this. This feature wouldn't change semantics of JavaScript, or add any JavaScript features, but would only add additional type capabilities to limit use of APIs in certain ways. In plain JS, before I ported a JavaScript project to TypeScript, I had an object manager that accessed properties prefixed with __ to denote that the properties were not to be used by the end user. The engine accessed the properties for internal implementation of the system.
@fatcerberus
Copy link

Seems like a duplicate of #5228 (C#-style internal), although your actual proposal looks semantically closer to C++‘s friend.

@trusktr
Copy link
Contributor Author

trusktr commented Dec 7, 2019

Ah, thanks for linking me to that. Yeah, this is totally closer to C++ friend than C# internal or Java "package protected".

I think this is more of an alternative than a duplicate, because internal (like C#) is not so useful in a language without any concept of a unit of compilation built from more than one file. Basically what I described at #5228 (comment).

@lorenzodallavecchia
Copy link

lorenzodallavecchia commented Dec 7, 2019

Since we are listing similar features in other languages, there are also these two:

  • InternalsVisibleTo for sharing internal visibility between assemblies in .NET.

  • @package annotation in Closure Compiler JSDoc, which retracts visibility to a directory. One of the few things I miss from the pre-TypeScript days.

@fatcerberus
Copy link

One problem with a declaration like @visibleIn(SomeClass) - what is SomeClass? This question is 100% clear in a language like C++ where all types are nominal, but TypeScript is structurally typed. So are we saying that all types shaped like SomeClass are friends, or all types named SomeClass are? Neither scenario is ideal IMO, and SomeClass only becomes a truly nominal type (the only ideal scenario) if it has private members.

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Dec 9, 2019
@RyanCavanaugh RyanCavanaugh changed the title feat request: cross-module property accessibility Add a visibility mechanism similar to friend or InternalsVisibleTo Dec 9, 2019
@trusktr
Copy link
Contributor Author

trusktr commented Dec 11, 2019

One problem with a declaration like @visiblein(SomeClass) - what is SomeClass?

In this case, SomeClass is in particular the exact class exported from the module where it is imported from. It isn't regarded as any sort of structural type, but merely tells the compiler that any code in SomeClass can access private members of instances of the current class. I guess it isn't about structural typing at all in this sense.

In some sense, it refers to SomeClass nominally, but usage of SomeClass isn't being type checked here. It would tell the engine "allow source from SomeClass class to access this here private or protected property on instances of this class".

@lorenzodallavecchia
Copy link

Maybe it could be useful to think of friend visibility at module level instead of classes. Modules are a native encapsulation concept in JavaScript, so it would probably feel more natural to reason on them, especially for the many folks that avoid classes in their programming style.

@trusktr
Copy link
Contributor Author

trusktr commented Dec 11, 2019

Yeah, totally. Maybe we can have both.

Because what if I have more than one class in a module, but I want the member visible only in a specific class? Or maybe even visible only in a specific method of a specific class.

Maybe we can come up with syntax for all those cases.

What about similar to above, but referring to a module:

export class Foo {
  visible in import('./SomeClass')
  private foo: number = 123
}

or

type Friend = typeof import('./SomeClass')

export class Foo {
  visible in Friend
  private foo: number = 123
}

or something?

Any other syntax ideas?

@EKashpersky
Copy link

EKashpersky commented Oct 12, 2020

IMO it makes sense to have visible keyword as a replacement to export (in some cases of course) like so:

Foo.ts

namespace NS {
  visible class Foo {}
}

PublicAPI.ts

namespace NS {
  export class PublicAPI {
    public constructor () {
      const tmp = new NS.Foo();
    }
  }
}

index.ts

const API = new NS.PublicAPI();

new NS.Foo(); // Cannot find class Foo within NS namespace

Reference tags omitted intentionally.

So if something is visible then we can access it within same namespace(for ex.), if export then we can access it outside.

Of course, it's a bit off the topic, I understand.
Still I think it'd allow to have same kind of functionality and having relatively small changes to the syntax.

@captainrdubb
Copy link

Please, in the name of the Common-Closure Principle, add this feature. Doesn't this align with the spirit of Typescript?

@branko-d
Copy link

branko-d commented Nov 26, 2021

Folder or file-level visibility or even a C++ style friend mechanism would be great for some types of React components.

In my case, I have a MobX-observable model class which drives the rendering of a purely-functional React component. Some of the observables in the model are an "implementation detail" necessary for the functioning of the component, while others represent its "public interface". It would be great if I could make the former visible to the component only, and latter visible to everyone. Perhaps something like: #41316 (comment)?

A typical example would be a picker: it can render an entire list of pickable items ("implementation detail"), and the consumer reads the currently picked item ("public interface"). The model needs both to properly "drive" the component, but the outside world will only ever be interested in the latter.

@TurboEncabulator9000
Copy link

TurboEncabulator9000 commented Nov 1, 2023

It would be nice to have this available for either module- or class-style development.

Maybe someone who never wrote a line of code until one day they went to a boot camp, and came out the other end a JS developer, and never wrote a single line in another language, thinks classes are dumb. However, a lot of us come from developing in various different OOP languages, and don't want to write blobs of functions with global variables floating around. We already have years/decades of experience before we start messing with JS/TS. We don't want to throw that away and lose all of the value it brings, and which TS already works quite hard to give us. (Really, what experienced OOP developer would want to write anything serious in plain JS if they didn't have to, and if TS wasn't around?)

If JS is going to be the "eight ways of doing everything" language, and if those who maintain it don't care to constrain it from that, and now there are essentially two distinct development methodologies, then why say only one should get this feature?

The more preeminent proposal is the internal keyword, but this doesn't prevent someone working within a repo who doesn't understand the intended architecture from misusing a method that's only public for want of a better mechanism.

For example, suppose I have these layers:

  • Controller
  • Service (called by Controller)
  • Adapter (factoried by the Service)

Nice and clean, easy to test, SOLID-compliant.

Service supplies some methods that would be useful by the Adapter, but it would be totally wrong for the Controller to call those methods. Therefore, only code "below" the Service (that is, in the Adapters) should call it.

However, suppose you have some developers who can't understand that. They see a new use case, and want to tack on some conditionals in the Controller that reach into the Service and call public methods intended only for the Adapters, thereby usurping their responsibilities. You can't control what they do because they don't all send their PRs to you, and those who will review those PRs don't get the layered design either.

You want to express this intended design using a "friend" mechanism, but you can't. Next option is internal, which is irrelevant to the problem at hand.

What are your alternatives? Either make the Service an abstract class which would be extended by an Adapter (in which case it becomes quite sprawling, and violates the Single Responsibility Principle), or you make several of its methods public and tolerate the risk that someone is going to do something goofy with them from a Controller.

@azerum
Copy link

azerum commented Dec 30, 2023

@TurboEncabulator9000 In your example, is it possible to define two interfaces that Service implements - one for methods exposed to Controller, and other for methods exposed to Adapters, and then, instead of making Controller and Adapters interact with the service directly, make them depend on the interfaces?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

9 participants