Open rbuckton opened 9 years ago
@errorx666 In the specification, there's no mention of using bracket notation in a method signature, so it seems like unintended behavior. But this comment suggests otherwise (sort of).
It’s valid JS syntax - effectively a computed property name but as a method rather than a plain property. If IntelliSense isn’t picking it up that sounds like a bug rather than a feature, but I could be wrong...
For a less buggy-looking solution, you can do this instead..
class Foo {
private foo() {}
}
const inst = new Foo()
inst['foo']()
..which is officially supported.
@DanielRosenwasser Bumping this again since TS is farther down the road after the original project ref work. This does keep coming up in both my open source work and "the day job". I just had a conversation today about how this would be nice to enable certain testing scenarios for our teams, who also don't want the APIs they need for testing exposed to consumers of their APIs, even within the same engineering org. The larger a project/team becomes, I think this sort of things becomes more and more important to have an official solution for, supported by compiler and tooling.
@EisenbergEffect agreed. I've taken to just including a second entry point (e.g. @some-org/somelib/testing
) that exposes the "internal" exports as a workaround for testing purposes, but something akin to .NET's internal
and the internalsVisibleTo
attribute would be far more ideal.
@EisenbergEffect @DanielSchaffer does a tool like API Extractor (which @octogonz and others at Microsoft develop) do what you need?
API Extractor is able to customize your .d.ts
files to do things like roll your files into one top-level .d.ts
file, strip away things marked as @internal
in your doc comments, and can be used to generate documentation pages.
Check out https://api-extractor.com/pages/setup/generating_rollups/#dts-rollup-with-trimming
It's usable for Aurelia, since we ship the APIs officially via npm. However, I'm not sure this works when the context is a larger set of engineers working together to build a unified product, but where the codebase is internally modularized and there are areas of ownership.
To be more concrete, imagine you've got 100 engineers, split into 10 squads of 10 engineers plus a PM each. Each one of those squads then has 3 or 4 sub-squads with 2-3 engineers each. Each sub-squad owns a set of APIs which are consumed by their peers, both inside their squad or outside of it, across the broader team. The sub-squad wants to be able to mark certain things as internal so that they can test them within their own domain, but don't have to worry about one of the other squads using them directly. The area of ownership often manifests as a simple folder within the project, facilitated by GitHub code owners policies. It's not necessarily a formal shipping division, such as an npm package.
Does that help clarify?
@EisenbergEffect I might be missing why the internal
modifier would facilitate building protected APIs any more than API Extractor in that organizational structure.
I might be missing why the
internal
modifier would facilitate building protected APIs any more than API Extractor in that organizational structure.
@DanielRosenwasser if I understood right, he's asking for fine-grained accessibility relationships between individual source files within a project folder. This is similar to C++ friend
classes. By comparison, API Extractor operates on NPM packages (under the idea that your code base is decomposed into lots of small library packages) -- this is more like C#'s internal
keyword that applies to entire .NET DLLs (but all the code within a single DLL has access to all the internals).
For what it's worth, @EisenbergEffect it seems to me that if a collection of files are cohesive enough to be "owned" by a team of people, it might make sense to separate that code into an NPM package. This approach has worked pretty well for our projects, which frequently have 100+ people working on them.
@octogonz In our case, for various reasons, we can't simply extract 100+ individual packages and shift to a monorepo. It's not that we haven't thought of that and don't want to do it. It's just not possible for this project at this time (and probably for a few years at least). So, I'm looking for ways to work within the current constraints.
All that aside, there is a desire to not have to bring additional tools into play to handle creating an accurate public API and get correct intellisense. So, even with something like API Extractor, it still seems like a work around for language features for which there is precedent elsewhere and which, if incorporated, would result in a better tooling experience.
FYI This isn't a sword I'm willing to die on. It's not a huge issue, more of a nice to have. But, I did want to see where we were at on this, since it seemed like it was being considered, but there was a desire to delay until after TS projects landed.
So, even with something like API Extractor, it still seems like a work around for language features for which there is precedent elsewhere and which, if incorporated, would result in a better tooling experience.
Agreed. It would be better for @internal
to be a first class feature of the TypeScript language. (The @alpha
/@beta
/@public
release designations perhaps are more in the province of an external tool.)
If for no other reason than the benefits for testing, I'd still say this is worth doing. On top of that, my project is also in a similar situation as @EisenbergEffect, where having the ability to control the accessibility on a per-file (or even a per-package, similar to Java) basis would considerably improve the typescript experience for my team.
In my opinion, the whole "internal" thing is related to the need to flatten declarations (#4433).
Both these features imply the fact that there will be an "inside" and an "outside" of the resulting library, usually because of some bundling step that will happen later, after the code is compiled, using tools such as webpack or rollup.
To achieve bundling, one usually has an entry module (index.ts
or similar) that re-exports symbols from other modules. The "external" API of the bundled library would then include everything exported, directly or indirectly, from the entry file. That external API is the one that I would expect to see flattened in a single .d.ts file, something that TypeScript is not capable of doing yet.
Exporting the flattened API in that way has two issues and internal
would help with both of them.
Keep properties of exported classes from appearing on the API.
To do that manually, one would have to define an interface (exported outside of the library) and a class (kept only inside). This is often too complex for just hiding a couple of methods because they are used for internal communication or for unit testing.
With internal
you would just mark methods/properties and the compiler would remove them from the flattened .d.ts. Of course, they are still available at runtime, but a conscious consumer of a TS library should not rely on non-API names. This is no different from how private
fields are still accessible at runtime but hidden in the definitions.
Prevent unintentional export of classes
The exported API is the result of a series of re-exports, possibly including full re-exports of intermediate index modules (sometimes called "barrels"). It is easy for a developer to forget about the whole API thing and unintentionally export a top-level name (class, but also function, etc.) that was meant to remain internal.
By marking classes internal
the developer would signal his intention to keep that name from appearing on the API. The compiler could then warn about unintentional exports for manual resolution by the developer.
I want to use this so that Cue.go
can be used by this Checkbox
parent class, but cannot be used when accessing Checkbox.OnChecked
externally. For now, I am considering this ugly code to be my solution:
type CheckedSignature = (isChecked: boolean) => void;
type InternalCheckedCue = Cue<CheckedSignature>;
type CheckedCue = Pick<InternalCheckedCue, "bind" | "unbind" | "unbindAll">;
class Checkbox {
/** Fires when the Checkbox is Checked */
public OnChecked: CheckedCue = new Cue<CheckedSignature>();
public SetChecked() {
(this.OnChecked as InternalCheckedCue).go(true);
}
}
How does one specify which classes/modules/files are able to see a given property?
With my idea over at https://github.com/microsoft/TypeScript/issues/35554, the usage is explicit about what code is allowed to access protected or private members.
I think that internal
like in C# (from what I've read, I haven't used C# before) is something that works for C# because the lib can be compiled into an "assembly" -- a grouping of the compiled code, like a DLL or packages in Java.
But in JavaScript/TypeScript, there are no package boundaries, only module (file) boundaries, and NPM package scope is out of question because there are other ways to consume code beside NPM packages, so I think it would be somewhat limiting for internal
to apply only within a file.
For example, what if I have code in 5 files that all need to access a private field in the class of a 6th file? It would not be ideal to have to merge the files all into one to achieve the sharing.
So for this to work well in JavaScript/TypeScript, I think we need something that is designed around sharing accessibility explicitly between certain other code in any files, considering that there are no such things as "package" boundaries in JS.
@lorenzodallavecchia You were trying to imagine how a "package" could be defined, above, but as you can tell, it is problematic. We can't prescribe everyone to use a certain form of bundling, or to consider a package to be based on an entry point, or to consider package boundaries to be NPM-style, or etc. End users of any library can import code in many different ways, and can import individual files arbitrarily, not necessarily from any bundle at all, and maybe not even from any sort of package as we know it (for example ES Modules allow us to import files from any URL that can be accessed through the internet and downloaded over HTTP).
I think it may be beneficial to think of methods that would be unique to JS/TS (similar yet distinct from features of other languages) for explicitly specifying shared accessibility with specific modules or things in modules, like #35554 starts to show.
What other syntaxes can we use? Can we do it with things other than class members?
Yes @trusktr you have a point in saying that there are possibly multiple concepts of "internal" and "external", With the example of webpack bundling I wanted to present one use case that I think is very common.
It is true that there may be other cases. One may want to declare "internal" scopes within his library just for the sake of encapsulation, without an actual runtime boundary. For example, I miss package-level protection (from Java and Closure) which can only be emulated with poor results using subdirectories in TS.
Even webpack itself can be configured in countless ways, for example splitting the codebase into multiple packages.
TypeScript should ideally have a way of describing the internal/external boundary in all these cases, and I think that is no easy task!
One possible idea is having a form of "scope" declaration that could be used for assigning module files to cross-cutting scopes inside the application. The internal
keyword would then refer to the internal side of those scopes. Think of the same as Java package
but without the restriction on the directory structure.
But still, I would like to see this feature interact seamlessly with the behavior of bundler, since they are a so common scenario.
And lastly, whatever the solution, I still think that this is somewhat related to API flattening. Simply put, one would want to flatten the API as seen from outside of the outermost boundaries of the library.
Help!
All my helper libraries are cluttered with public _internal_doSomething()
methods.
This is ugly.
However, less ugly than calling private methods with bracket notation as mentioned above (which is barbaric).
Given that JavaScript has recently added support private fields, I'm not sure if adding new TypeScript-specific access modifiers is a good call for future proofing the langauge.
I can see how adding new features along the lines of the current access modifiers, which might end up deprecated in favor of native private properties, might be a bad idea. But the need for some well-designed alternative to @internal
remains, even if it doesn't look like a new access modifier.
Here is the cleanest workaround I have devised so far:
export class Type {
protected __shared: number;
constructor() { }
}
export interface Type {
__shared: number;
}
import * as internal from "./Internal"
import Type from "./Type";
let instance = new Type();
(<internal.Type><any>instance).__shared = 1000;
The key is that we need shared properties/methods between classes that are hidden from public APIs
If anyone has an even cleaner typed workaround, let me know.
public
is exposed to users through IntelliSense, but protected
won't allow enough access, and use of <any>
or instance['__shared']
breaks typing.
@jgranick could this approach be modified a bit to use declaration merging? E.g. if Internal.ts
was Internal.d.ts
, then when the declaration file was used, you'd have access to the internal members without the need for type casting. Without it Internal.d.ts
, they're still inaccessible.
I have another thought here. From what I understand, an internal
modifier means that method is accessible until it is used by an outsider. But, what is an outsider? I assume we just mean after typescript has built a project. So, what if we had a simple @stripmethod
doc comment above methods? Then when typescript declarations are built we remove those methods from the declarations. I think both defining the internal methods and marking them as 'ignored' in declaration files feels like unnecessary complexity. If we simply strip the methods, we might even be able to handle this from outside typescripts compiler.
[edit]
Wait. Is this solved already? There appears to be a --stripInternal
compiler option https://stackoverflow.com/questions/32188679/how-does-a-typescript-api-hide-internal-members.
//Class will not be visible
/** @internal */
export class MyHelperClass {
}
export class MyPublicClass {
//Method will not be visible
/** @internal */
helperMethod() {}
}
// Binding will not be visible
/** @internal */
export const MyValue = 5;
tsc --stripInternal
@andykais If the project is distributed as d.ts then yes, this hides the properties but the properties are still visible in IntelliSense when working with .ts. If you use a common prefix such as "_" then it is sorted before your public members.
I propose that if an internal
keyword were adopted, it would behave like /** @internal */
when generating type declarations, public
when compiling but protected
in IntelliSense hints.
@jgranick By protected do you mean that it will not show up in intellisense?
My preference for internal
would be:
public
within the project where the code is defined.Another way I think of it is that it's a normal public property/method/etc in JavaScript. There's no runtime component to this. It's just that it's completely hidden by compiler/intellisense/tooling for any code outside of the project where it is defined.
@EisenbergEffect I would be happy with your proposal if the concept of "inside" or "outside" the project is clear to the compiler. I was concerned this was a foreign concept?
If there is not a way to distinguish cleanly between "inside" or "outside" the project then my proposal might be a second best:
public
.protected
visibility)I think it's been mentioned elsewhere in this thread, but I would add to @EisenbergEffect's description:
This is both a way to provide a level of runtime enforcement for internal types/functions/variables, and provides a runtime benefit (smaller names) with no downside. It's also part of the reason that I think internal
should be standardized (so tools can use the additional information that it provides).
Just my +1 for internal. I was surprised to find out that Typescript does not support this :(
Another use case to consider, which is specifically mine ;), is having classes share a common abstract parent class. You may want to distribute/document their children, but neither the parent itself nor its methods/properties...
You may want to distribute/document their children, but neither the parent itself nor its methods/properties...
That's simlar to "package protected" as called in other languages. What we need in JS and TS is "module protected" and "module private". Seems that https://github.com/microsoft/TypeScript/issues/35554 would suit your needs more, and it is better than the internal
idea here because it limits scope of variables to certain modules, rather than making everything visible in the whole project (protected and private is still valuable within projects, and replacing protected
with internal
makes them public
within a project which is not ideal).
+1 for internal. I would also like to see this for global functions, variables, etc which also have the export keyword, so that they can be accessed from other files in the same package, including tests (which requires an export declaration,) but 'hidden' externally. The internal keyword would communicate clearly to future maintainers the intent of the associated construct.
On our team, we use the jsDoc /* @internal / comment quite a lot to designate internal functions, but there are some files that we exclude from the compilation used to generate the index.d.ts file, and in those files, we don't always consistently use the @internal comment (though we probably should.) If someone in the future then does something such that those files are included in the index.d.ts compilation, those functions would be exposed.
An internal keyword with the characteristics described would be especially useful to better communicate intent, and to enforce it as best as Typescript can.
The only time I need to access private methods from the outside would be within a unit test. As we separate the tests from the code how would the "internal" keyword cope with this ?
@Xample Real private methods should not be tested directly, since they are implementation details.
However, it may happen that some methods are "external" from the perspective of a given class, but still "internal" with regard to your libraries. In these cases, it does make sense to test them and this is the exact scenario where the internal
keyword would be useful.
So basically you would use internal
for all things that are part of the API of a single .ts file, but not part of the API of the overall library that you are building.
Real private methods should not be tested directly, since they are implementation details
Please, see this comment.
Since this seems to go nowhere anytime soon and I am currently in need for this, as I am maintaining a TypeScript Port of a C++ Project (Box2D), which uses C++ friend classes, I need an alternative way to strip away access from the outside.
So I've written a small post-processing tool to help me do this by using recast to transform the (generated) .d.ts files after they've been built. This works by adding a jsdoc tag @internal
to your properties, methods and exports.
This is better than stripInternals, since the identifiers still remain in the d.ts files, so they will complain on extended classes, etc. Of course, you'll have the extra work of using jsdoc, but an internal
access level would require almost the same amount of work and when the language feature gets implemented, you'll have it easier by just searching for the jsdoc tag and replacing it. Could probably even write a migration tool for that. Doesn't seem to be that much work with recast.
I still need to add these jsdoc tags to my Box2D port, so I haven't tried this on a large scale project yet, but the sample snippets work just fine. Give it a try if you like: https://github.com/Lusito/idtsc
Feel free to add feedback in the project's issue tracker. Check out the readme for a sample input/output
Looking forward to a language feature, so I can discontinue this project.
I like this concept of an internal
access specifier for the reasons @johncrim indicates because a symbol marked as an "internal detail" can have its face be ripped off and be minified/uglified to death. That is a pretty nice optimization hint!
I have a separate concern that I feel may have been conflated a bit in the overall discussion above and would like to disambiguate it here.
While TDD'ing code, I will place my *.test.ts unit test files in the same directory as their systems under test, which seems to be a fairly common convention.
I'd like to be able to mark some methods of my class with an access specifier other than public
and still have those members be visible within the test module. JSDoc has this concept, known as package-private: https://jsdoc.app/tags-package.html
The idea is that modules in the same directory can access the class member as if it was public, but it is private to modules in every other directory. I like the terminology JSDoc uses for this access specifier, package
, so I propose that be what Typescript uses as its keyword unless you have other intents for package
.
Finally, this concept differs from #321 where the proposal is for a module
access specifier, which is to be module-private. I imagine that internal
could be mixed with access specifiers public
, private
, protected
, package
and module
. And if another access specifier is not mixed with internal
, public
should be the implicit access level as @EisenbergEffect recommends.
still greatly required, we have a multi project solution with TypeScript (using tsconfig project references) and currently we leak all the "should be internal" methods into the other projects, we cannot mark them as private as they are required to be used internally by the project that owns them. internal
operating similar to c# would greatly improve the review/maintainability of this codebase and I don't believe I am the only person in this situation otherwise why would TypeScript project references be advertised as a good solution to separate concerns?
+1 for this.
+1 for this as well.
I think this would be very helpful +1
While working on a set of cooperating classes, I was looking for the equivalent of C++ friend
classes, and stumbled on this internal
proposal. This mechanism is actually much better than friend
as it encourages encapsulation at the module level as well as splitting code into small modules of related functionality.
If we simply strip the methods, we might even be able to handle this from outside typescripts compiler. Wait. Is this solved already? There appears to be a --stripInternal compiler option
The internal
modifier would be a syntax sugar for /* @internal */
. I think this is the best solution.
While working on a set of cooperating classes, I was looking for the equivalent of C++ friend classes...
I like this possibility too.
The
internal
modifier would be a syntax sugar for/* @internal */
. I think this is the best solution.
I'd like to add that a huge shortcoming of using /* @internal */
today is that it is "dumb". It just erases members from the final .d.ts
files, with no regard for API correctness.
The stripped .d.ts
files may contain errors like:
import..from
declarations that refer to empty .d.ts
files, which a downstream project will not recognize as valid ESM.The only way to protect from those issues today is to use a tool like api-extractor for checking that the API is consistent.
Ideally, the new internal
keyword should do the consistency checks while checking the source files and emit the necessary diagnostics. For example, on encountering a non-internal
method using an internal
type, TypeScript would advise the programmer to either
internal
too,internal
from the offending type,internal
.On a related note, a pain point would be having to rewrite all those internal type aliases that we declare just to make our code more readable, but that we don't want to export from our files. Maybe TypeScript should automatically expand those internal type aliases into their definition.
The interaction between internal
and API consistency is the reason I think this issue has something in common with definitions bundling (#4433).
One idea that would be even more explicit and not require any special file/directory handling, would be if I could mark a specific method/property as being allowed by specific classes, e.g.:
class Bar {
run(foo: Foo) {
foo.run() // good
foo.run2() // type error
}
}
class Baz {
run(foo: Foo) {
foo.run() // type error
foo.run2() // type error
}
}
class Qux {
run(foo: Foo) {
foo.run() // good
foo.run2() // good
}
}
class Foo {
// Foo and Qux could be in the same file or imported from anywhere
internal(Bar, Qux) run() {
programming.solveOneOffError(n => n+1);
}
// different methods/properties of the same class could be internal to different things; they'd all be independent
internal(Qux) run2() {
this.jog();
}
}
I like @sdegutis' idea, which would essentially be internal
that can be narrowed to act more like friend
as needed. You'd get the best of both worlds.
Some seem to think this proposal should be exclusive of the friend
idea because it assumes only one useful boundary (package level) for visibility control. I say there are two such boundaries. Inappropriate coupling can come from inside a package at least as easily as it can come from outside.
As originally proposed, internal
would help protect members from inappropriate access from outside a package, but would do nothing to protect them from inappropriate access from within the same package. If anything, people working on a package will naturally encounter more opportunities to misuse access than someone whose only exposure to the package is that they typed npm i
and read about the public interface on GitHub.
Now, if your project is 100% yours, or is only worked on by you and other people who understand a unified vision, maybe it makes no difference to you. If that's you, you're very lucky, and I envy you.
In reality, a big organization is going to have a lot of developers who have varying levels of understanding of good architecture, design patterns, etc. Some sleep with the Gang of Four book under their pillows. Others heard of design patterns once three years ago, and... that's it. Therefore, I allege that it's useful to have a mechanism for sharing access to certain things within a subset of a package, but not necessarily to the whole entire package.
For example, suppose you have three layers:
Service has some methods useful both to itself and to Adapter, but which would be inappropriate to be called by Controller. Lacking friend
or some substantially similar method, those methods could be made public
so that Adapter can call them.
Of course, sooner or later, someone comes along who doesn't understand this design, isn't inclined to learn, and is used to adding conditionals with additional code in the most convenient-looking places. Their behavior is dictated by an energy function which only cares about how many points they can burn down in this sprint. More points = more better! Whatever happens in three months or a year is irrelevant to this energy function.
They tack on some code to Controller which calls methods on Service which were intended for use only by Adapter. This makes perfect sense to them. You know it's going to be harder to maintain this way, but they don't, and whoever reads their PRs won't care either. You can't read every PR organization-wide, and you may not be empowered to decline them anyway. Now a Controller that should be quite thin, and shouldn't be tightly coupled with implementation details at lower levels of abstraction, is hard-wired directly into that stuff.
Possible solutions:
Service "eats" Adapter. Adapter's properties and methods are all moved into Service. What would otherwise have been a concretion of Adapter is now a concretion of Service. This would mash two design patterns together in a way that isn't so great. The resulting class would be much less compliant with the Single Responsibility Principle. What might be just over the edge of "uncomfortably large" today may sprawl into a monstrous God Object by the time a few years have gone by.
The methods/variables you wanted to "friend" could be refactored out to some other class. Service and Adapter would have to extend, implement, or compose with that class. You wouldn't need friend
or internal
. If you use the extend/implement approach, at least Controller can't mess with those things directly, but then you have to play with inheritance in a way you wouldn't have to with friend
or internal
. If you use composition instead, then Controller is free to import the class you only wanted to be used by Service and Adapter.
The
internal
modifierOften there is a need to share information on types within a program or package that should not be accessed from outside of the program or package. While the
public
accessibility modifier allows types to share information, is insufficient for this case as consumers of the package have access to the information. While theprivate
accessibility modifier prevents consumers of the package from accessing information from the type, it is insufficient for this case as types within the package also cannot access the information. To satisfy this case, we propose the addition of theinternal
modifier to class members.Goals
This proposal aims to describe the static semantics of the
internal
modifier as it applies to members of a class (methods, accessors, and properties).Non-goals
This proposal does not cover any other use of the
internal
modifier on other declarations.Static Semantics
Visibility
Within a non-declaration file, a class member marked
internal
is treated as if it hadpublic
visibility for any property access:source.ts:
When consuming a class from a declaration file, a class member marked
internal
is treated as if it hadprivate
visibility for any property access:declaration.d.ts
source.ts
Assignability
When checking assignability of types within a non-declaration file, a class member marked
internal
is treated as if it hadpublic
visibility:source.ts:
If one of the types is imported or referenced from a declaration file, but the other is defined inside of a non-declaration file, a class member marked
internal
is treated as if it hadprivate
visibility:declaration.d.ts
source.ts
It is important to allow assignability between super- and subclasses from a declaration file with overridden members marked
internal
. When both types are imported or referenced from a declaration file, a class member markedinternal
is treated as if it hadprotected
visibility:declaration.d.ts
source.ts
However, this does not carry over to subclasses that are defined in a non-declaration file:
declaration.d.ts
source.ts