Repository navigation
Proposal: The internal modifier for classes #5228
Description
Activity
- addedIn DiscussionNot yet reached consensusNot yet reached consensus
on Oct 12, 2015 Would the following also error:
declaration.d.ts
class C { internal x(): void {}; }
source.ts
/// <reference path="declaration.d.ts" /> class D extends C { x(): void {}; // error? }
How does this differ from the more common language semantic
protected?The non-goal is not to address this in other declarations, but couldn't this start to becoming confusing for someone trying to utilise a package or library? For example, I would assume the following you would want to work this way:
declaration.d.ts
interface InterfaceC { } class C implements InterfaceC { internal x(): void {}; }
source.ts
/// <reference path="declaration.d.ts" /> class D implements InterfaceC { } const c: C = new D(); // error, but why? I implemented the interface properly.
When targeting ES6, do you see any changes, considerations in the emit?
Do you have a more practical use case of where this pattern would resolve significant problems? I can understand how for a consumer it could clean up an API, but at the same time, it could be really confusing where you are "hiding" things that will always exist at runtime from compile time. It almost seems like a six of one, half a dozen of another.
An alternative to modifiers is:
package.d.ts
/// <reference path="declaration.d.ts" /> internal { C.x; }An
internal {}block marks which parts of the public api are hidden.+1 to something like this
alisabzevari commented
on Nov 8, 2015 ContributorMore actions+1 for this.
ToddThomson commented
on Jan 19, 2016 ContributorMore actionsInternal modifier for classes and their properties/methods within a component/program would allow greater scope for Typescript identifier shortening.
+1 for this proposal.I think I'd want to extend
internalto also work on classes.It would work pretty much like the
--stripInternalcompiler flag works now, except without the need to add JSDoc to the class.Additionally, though this may need some exploring, it could be used to keep classes within a module. So for example:
//file1.ts module A.B { internal class C { } var c: C = new C(); //Works } //file2.ts module A { var d: A.B.C = new A.B.C(); //Error because C is only available inside A.B module scope. } //file3.ts module A.B { var e: A.B.C = new A.B.C(); //Works, back in scope }
- changed the title
[-]Proposal: The internal modifier[/-][+]Proposal: The internal modifier for classes[/+]on Jan 28, 2016 tetsuharuohzeki commented
on Feb 1, 2016 More actionsI'm polishing classes' members' openness of RxJS like ReactiveX/rxjs#1244, then I wanted this feature to control openness in a complexed class dependencies.
JonathanMEdwards commented
on Mar 3, 2016 More actions+1 Having to write internal all over the docs is currently my biggest pain point with TS. Which is another way of saying that things are pretty good - good job guys :)
Reacted by levp, Anton Venema, Amir Arad, Reese Walton, Rob Eisenberg, Alec Larson, resynth1943 and vfsfitvnm+1
any progress on this?Reacted by Patrik Minder, levp, Coffee Boy and Sebastian Obentheuer+1, this would be awesome to have.
Reacted by Patrik Minder, levp, Anton Venema and Coffee BoyThis would be really nice for Angular components where you need properties/methods to be public to be accessed by the template, but want them to be private to other users that are consuming your components. The current
/** @internal */feature is problematic because it causes types to fail to satisfy implemented interfaces when thed.tsis compiled downstream.Reacted by Austin FitzCorbett and branko-d+1 This would really help with testing; right now you have to make methods public that you really just want to be visible for testing but not part of the public API.
Reacted by Petr Koutny and Nurgazy Nazhimidinov67 remaining items
+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.
Reacted by menelike, fughur, Lorenzo Dalla Vecchia and Nurgazy NazhimidinovThe 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 ?
lorenzodallavecchia commented
on Dec 19, 2020 More actionsFlavien Volken (@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 theinternalkeyword would be useful.So basically you would use
internalfor 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.Reacted by ExE Boss and Nurgazy NazhimidinovReal 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
@internalto 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
internalaccess 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.
Reacted by Amish ShahI like this concept of an
internalaccess specifier for the reasons John Crim (@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
publicand still have those members be visible within the test module. JSDoc has this concept, known as package-private: https://jsdoc.app/tags-package.htmlThe 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 forpackage.Finally, this concept differs from #321 where the proposal is for a
moduleaccess specifier, which is to be module-private. I imagine thatinternalcould be mixed with access specifierspublic,private,protected,packageandmodule. And if another access specifier is not mixed withinternal,publicshould be the implicit access level as Rob Eisenberg (@EisenbergEffect) recommends.Reacted by John Crimstill 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.
internaloperating 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?Reacted by Hurmenyi, Anna Kovarska, MadSkunk, Eric Swann, Luke Brandon, Levin Rickert, Michael Waddell, Will B, dQw4w9WgXcQ, kpietraszko and 1 more+1 for this.
Reacted by Hadrien Milano, Lorenzo Dalla Vecchia, Matthias Kunnen, Seva Golovanov, Richard and mdornseifReacted by Brian Kim+1 for this as well.
Reacted by the_number_twoReacted by Hadrien Milano, Lorenzo Dalla Vecchia, Matthias Kunnen, Holger Jeromin, Seva Golovanov, Richard and mdornseifReacted by Brian KimI think this would be very helpful +1
Reacted by Hadrien Milano, Lorenzo Dalla Vecchia, Matthias Kunnen, Holger Jeromin, Seva Golovanov, Richard and mdornseifWhile working on a set of cooperating classes, I was looking for the equivalent of C++
friendclasses, and stumbled on thisinternalproposal. This mechanism is actually much better thanfriendas it encourages encapsulation at the module level as well as splitting code into small modules of related functionality.Reacted by whzx5byb, the_number_two and ReeceGordonIf 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 optionThe
internalmodifier 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.
lorenzodallavecchia commented
on Oct 6, 2021 More actionsThe
internalmodifier 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.tsfiles, with no regard for API correctness.
The stripped.d.tsfiles may contain errors like:- methods referring to missing types in their signatures,
import..fromdeclarations that refer to empty.d.tsfiles, 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
internalkeyword should do the consistency checks while checking the source files and emit the necessary diagnostics. For example, on encountering a non-internalmethod using aninternaltype, TypeScript would advise the programmer to either- making the method
internaltoo, - remove
internalfrom the offending type, - use on the method signature a different (wider) type that is not
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
internaland API consistency is the reason I think this issue has something in common with definitions bundling (#4433).Reacted by Filipe Beck, Daniel Rosenwasser, ikokostya, Will B, dQw4w9WgXcQ and Dan ChuOne 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(); } }
Reacted by Filipe Beck, noukenolife, Dan Chu, J'C Kabunga, Lucas Choi, TurboEncabulator9000, chocolateboy and LainI like sdegutis' idea, which would essentially be
internalthat can be narrowed to act more likefriendas needed. You'd get the best of both worlds.Some seem to think this proposal should be exclusive of the
friendidea 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,
internalwould 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 typednpm iand 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:
- Controller
- Service (instantiated by Controller)
- Adapter (factoried by Service)
Service has some methods useful both to itself and to Adapter, but which would be inappropriate to be called by Controller. Lacking
friendor some substantially similar method, those methods could be madepublicso 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
friendorinternal. 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 withfriendorinternal. If you use composition instead, then Controller is free to import the class you only wanted to be used by Service and Adapter.Reacted by Czandal
The
internalmodifierOften 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
publicaccessibility modifier allowstypes to share information, is insufficient for this case as consumers of the package have access
to the information. While the
privateaccessibility modifier prevents consumers of the packagefrom 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 the
internalmodifier to class members.Goals
This proposal aims to describe the static semantics of the
internalmodifier as it applies to members of a class (methods, accessors, and properties).Non-goals
This proposal does not cover any other use of the
internalmodifier on other declarations.Static Semantics
Visibility
Within a non-declaration file, a class member marked
internalis treated as if it hadpublicvisibility for any property access:
source.ts:
When consuming a class from a declaration file, a class member marked
internalis treated asif it had
privatevisibility for any property access:declaration.d.ts
source.ts
Assignability
When checking assignability of types within a non-declaration file, a class member marked
internalis treated as if it hadpublicvisibility: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
internalis treated as if it hadprivatevisibility: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 adeclaration file, a class member marked
internalis treated as if it hadprotectedvisibility: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