Repository navigation
Suggestion: Module level privacy for types #15465
Description
Activity
dup? #321
Aleksey-Bykov Yes, it is definitely a dup of #321. And I made this suggestion for the same reason you did :-). I originally searched for "abstract type" and "opaque type" and ended up empty, that's why I did not find your suggestion. I changed the
privatemodifier above tomoduleso now the suggestions are identical. Would it be OK if I pasted my suggestion above into your issue and closed this one?i'd keep this one open so the problem gets more attention
Aleksey-Bykov It seems the typescript team is reluctant to add this. And I can understand why. In my experince, when you maintain a product, the cost of adding a feature is low compared to the cost of maintaining the added complextiy it incurs in all future development. I guess the main userbase of TS are mainstream OOP style programmers. So adding features for a low (but hopefully growing) number of TS users doing lightweight FP style development with modules has low business value.
Nevertheless, I think this suggestion is still valid for the proposed use-case, even if that use-case is currently a bit narrow. I guess we'll have to see if there are other mainstream OOP use-cases that it can solve or if more developers adopt ES modules for FP style programming and would benefit from having the compile-time checks this suggestion enables.
Reacted by Maksim Sinik- addedToo ComplexAn issue which adding support for may be too complex for the value it addsAn issue which adding support for may be too complex for the value it adds
on May 8, 2017 RyanCavanaugh commented
on May 8, 2017 MemberMore actionsSo the main problem here is that if you ask 10 developers what constitutes a module-level boundary, you'll get 11 answers (namespace! compilation unit! folder! ES6 module! file! npm
@scope! tsconfig! ...). We could support 11 different ways of drawing these boundaries but never be able to cover everything and still confuse the heck out of everyone.We're sympathetic here -- we use a weird
@internalcomment which clearly indicates some kind of need, but our definition of@internal(public to us in a different compilation but hidden, not even private, to external consumers) is even more esoteric than whatever boundary we'd ship if you put a gun our heads and made us do something.This all gets even worse because you actually do need to emit "internal" things, sometimes, to prevent incorrect assignability or derived class private field overwrites.
Unfortunately I don't have an authoritative "you should do X instead" here because I think it really depends on what the use case is.
privateclass fields come the closest and you can definitely usefully lie to your consumers about how your types are actually constructed. Or@internalhacks, etc, it just depends.Reacted by Ilie Ciotirhaving 11 questions and giving 1 COMPLETELY UNRELATED answer never stopped you before (#13002) did it? how come you are all lost now?
... lie to your consumers...
i love it! can i be your friend please! please!
Ryan Cavanaugh (@RyanCavanaugh) I think "module" has a distinct meaning within ES and by extension also TS. But I see what you mean. I have a need in some projects where I have a monorepo which contains "libraries" that have internals and a public API. For this I made a linting rule to only allow import of
index.tsfrom some parts (folders) of the repo into other parts of it. That way I can just re-export the public API inindex.tswhile the other files/modules becomes "internal".Although there seems to be different needs for boundary (namespace, compilation unit, folder, ES6 module, file, npm @ scope, tsconfig! ...) I would think all of them could be boiled down to having a boundary for a set of files?
Because the needs are different, I guess one solution would be to make it possible for the end-user to define sets of files himself, perhaps in tsconfig. Then there would be no need to decide on a common definition of the boundary for
internal(other than it operates on a set of files/modules). I have only quickly skipped through #5228 so maybe this has already been discussed.- locked and limited conversation to collaborators
on Jun 14, 2018
Module level privacy for types
It is sometimes useful to declare a type within a module and then have that module handle all of the operations on that type. This way the consumers of the module will not know the internals of the type and cannot couple themselves to its properties. But we still get type safety when calling the module's functions. This is especially useful in a FP programming style where we only use functions and types packaged into modules.
Goals
To make the internal of types private to the module they are declared in.
Non-goals
To introduce nominal typing in order to solve the goal.
Example scenario
Consider a
date.tsmodule that handles aDatetype. The internals of theDatetype is private to thedate.tsmodule. This is declared by amodulemodifier on theDate's members that this suggestions adds support for. But the type is still exported so other modules know of theDatetype. This means type-safety is possible when calling the methods of the Date module.date.ts
Consumption
Usefulness
Using this approach it will become much easier to refactor the
Datetype. Let's say we decide it is better to have it store ticks since epoch:Without privacy, other modules may have directly read the
Date.yearsproperty instead of going through ouryear()function making our refactoring hard.Similar features in other languages
Ocaml: abstract type
Reason: abstract type
Elm: opaque type
F#: signature files
Haskell: export lists
Work-arounds
Class
It has been noted that in typescript
classcan be used to emulate the idea of this proposal. But that is only true because typescript adds theprivatemodifier toclass. In plain JS/ES it would not make sense to use both a module and aclassas modules alone provides an idiomatic construct for packaging static functions together. So usingclassin plain JS would only add another level of nesting/complexity/syntax without adding any value.Consider this plain JS/ES module:
date.js
In the above module it does not make sense to use
classbecause in JSclassis neither immutable nor has the ability to be opaque. Typescript currently has the ability to express all the notes in the comments except the "considered to be opaque" part. For example "considered to be immutable" could be expressed asreadonlyon the type's properties. This proposal is about being able to express the "considered to be opaque" part in the typescript type system without having to modify the original JS program to useclass.Another big disadvantage of using
classas a data record is that it cannot surviveJSON.stringify(),JSON.parse()round-trips. This makesclassimpractical to use in many real-world scenarios such as for types in a serializable Redux state, or any types you want to put in local storage, or send over the network.The closest
classwork-around that is available in TS today is what Marin Marinov (@gcnew) describes here. This example also survives round-trips. However it requires us to declare a class and take special care to not use all class features. It also requires code to re-export all the class methods from the module. So it involves quite a lot of syntatic work-around noise compared to the idiomatic JS example above. This suggestion makes it possible to write idiomatic JS instead of adjusting the JS to fit the current privacy features in TS.Prefix
Another suggested work-around is to prefix the fields in the record with
_in order to mark them as internal. This work-around is much better in the sense that it does not alter the way you write the code. But there are two drawback. The first being that obviously this would not enable any compile-time checks. The second being that although_is a convention commonly used for private members of aclassin OOP style programming, there is (AFAIK) no such tradition for FP style record types. So the use of_in this case becomes unconventional.Related suggestions
#321 is an earlier suggestion for the same thing.
Abstract/opaque types was originally proposed in #15408 which has relevant discussions leading up to this proposal.
The
internalmodifier in #5228 is similar. It adds privacy on the package level. If each module was a package andinternalwas available for types in addition toclassthen it would be identical to this suggestion.