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

Suggestion: Module level privacy for types #15465

Closed
jonaskello opened this issue Apr 29, 2017 · 8 comments
Closed

Suggestion: Module level privacy for types #15465

jonaskello opened this issue Apr 29, 2017 · 8 comments
Labels
Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds

Comments

@jonaskello
Copy link

jonaskello commented Apr 29, 2017

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.ts module that handles a Date type. The internals of the Date type is private to the date.ts module. This is declared by a module modifier on the Date's members that this suggestions adds support for. But the type is still exported so other modules know of the Date type. This means type-safety is possible when calling the methods of the Date module.

date.ts

export interface Date { 
    module readonly day : number,
    module readonly month : number,
    module readonly year : number
};

export function createDate(day: number, month: number, year: number): Date {
    return {day, month, year};
}

export function diffYears(date1: Date, date2: Date): number {
    return dat1.year - date2.year
}

export function year(date: Date): number {
    return date.year;
}

Consumption

import * as Date from "./date";

const date1 = Date.createDate(2017, 1, 1);
const date2 = Date.createDate(2018, 1, 1);
const year = Date.year(date);
const diff = Date.diffYears(date1, date2);
// This will give an error because the year property is private to date.ts
const year = date.year; 

Usefulness

Using this approach it will become much easier to refactor the Date type. Let's say we decide it is better to have it store ticks since epoch:

export interface Date { 
    module readonly ticks : number,
};

Without privacy, other modules may have directly read the Date.years property instead of going through our year() 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 class can be used to emulate the idea of this proposal. But that is only true because typescript adds the private modifier to class. In plain JS/ES it would not make sense to use both a module and a class as modules alone provides an idiomatic construct for packaging static functions together. So using class in plain JS would only add another level of nesting/complexity/syntax without adding any value.

Consider this plain JS/ES module:

date.js

/**
* @param {number} day
* @param {number} month
* @param {number} year
* @returns {Date}
* NOTE: Return type is to be considered immutable and opaque.
*/
export function createDate(day, month, year) {
    return { day, month, year };
}

/**
* @param {Date} date 
* @returns {number}
*/
export function year(date) {
    return date.year;
}

In the above module it does not make sense to use class because in JS class is 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 as readonly on 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 use class.

Another big disadvantage of using class as a data record is that it cannot survive JSON.stringify(), JSON.parse() round-trips. This makes class impractical 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 class work-around that is available in TS today is what @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 a class in 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 internal modifier in #5228 is similar. It adds privacy on the package level. If each module was a package and internal was available for types in addition to class then it would be identical to this suggestion.

@zpdDG4gta8XKpMCd
Copy link

dup? #321

@jonaskello
Copy link
Author

@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 private modifier above to module so now the suggestions are identical. Would it be OK if I pasted my suggestion above into your issue and closed this one?

@zpdDG4gta8XKpMCd
Copy link

i'd keep this one open so the problem gets more attention

@jonaskello
Copy link
Author

@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.

@RyanCavanaugh RyanCavanaugh added the Too Complex An issue which adding support for may be too complex for the value it adds label May 8, 2017
@RyanCavanaugh
Copy link
Member

So 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 @internal comment 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. private class fields come the closest and you can definitely usefully lie to your consumers about how your types are actually constructed. Or @internal hacks, etc, it just depends.

@zpdDG4gta8XKpMCd
Copy link

having 11 questions and giving 1 COMPLETELY UNRELATED answer never stopped you before (#13002) did it? how come you are all lost now?

@zpdDG4gta8XKpMCd
Copy link

... lie to your consumers...

i love it! can i be your friend please! please!

@jonaskello
Copy link
Author

@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.ts from some parts (folders) of the repo into other parts of it. That way I can just re-export the public API in index.ts while 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.

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds
Projects
None yet
Development

No branches or pull requests

4 participants