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

Consider adding a module level visibility modifier. #321

Open
zpdDG4gta8XKpMCd opened this issue Jul 31, 2014 · 25 comments
Open

Consider adding a module level visibility modifier. #321

zpdDG4gta8XKpMCd opened this issue Jul 31, 2014 · 25 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@zpdDG4gta8XKpMCd
Copy link

Currently to protect integrity of data one can only use classes with private or protected fields. Classes are harder to work with compared to object literals. Being able to specify that certain fields are invisible to outside modules would be a valuable addition to support programming in functional style in TypeScript.

So what I am suggesting is to be able to specify whether a field of an interface is exported from a module or not. This also means that the instance of such interface can only be created within the module it is declared.

The similar features can be found in:

@sophiajt
Copy link
Contributor

Would be good to see the motivating examples behind this and what JavaScript patterns this would help enable. Some general tips for proposals: https://github.com/Microsoft/TypeScript/wiki/Writing-Good-Design-Proposals

@zpdDG4gta8XKpMCd
Copy link
Author

I hear your call for more formal proposal. I will try my best as soon as I have time for it. Meanwhile here is an example. I want to get a lightweight implementation of a doubly linked list in TypeScript. The most lightweight version as far as I can see would be based on object literal rather than classes. It would look something like this:

// module: DoublyLinkedList

export interface Node<a> {
    /* module */ left: Node<a>;
    value: a;
    /* module */ right: Node<a>;
}
export interface List<a> {
    /* module */ left: Node<a>;
    /* module */ right: Node<a>;
}
function createNode<a>(left: Node<a>, value: a, right: Node<a>) {
    var node = { left: left, value: value, right: right };
    if (left != null) { left.right = node; }
    if (right != null) { right.left = node; }
    return node;
}
export function createEmptyList<a>() : List<a> {
    return { left: null, right: null }
}
export function createList<a>(value: a): List<a> {
    var node = createNode(null, value, null);
    return { left: node, right: node };
}
export function addRight<a>(list: List<a>, value: a) : Node<a> {
    return createNode(list.left, value, null);
}
export function addLeft<a>(list: List<a>, value: a) : List<a> {
    return createNode(null, value, list.right);
}
// many other functions to manipulate a double linked list
export function isLast(node: Node<a>) { 
    return node.right == null;
}

Now notice that the internal properties like left and right of the List and Node interfaces are exposed. These properties are not supposed to be seen. But there is no way to hide them unless we switch from lightweight object literals to instances of classes and use private modifier. So clearly the internals of the list and node are vulnerable to unintended or deliberate changes that we wish we could prevent to maintain the integrity.

Notice the \* module *\ comments in the Node and List interfaces. This is what I wish the module visibility modifiers looked like. There is no need to introduce another keyword, as we reuse some already existing one.

@RyanCavanaugh
Copy link
Member

You can emulate this somewhat simply today:

module Impl {
    interface Node<a> extends Public.Node<a> {
        left: Node<a>;
        right: Node<a>;
    }
    interface List<a> extends Public.List<a> {
        left: Node<a>;
        right: Node<a>;
    }
    export function createNode<a>(left: Node<a>, value: a, right: Node<a>) {
        var node = { left: left, value: value, right: right };
        if (left != null) { left.right = node; }
        if (right != null) { right.left = node; }
        return node;
    }
    export function createEmptyList<a>() : List<a> {
        return { left: null, right: null }
    }
}
module Public {
    export interface Node<a> {
        value: a;
     }
     export interface List<a> {
         /* empty */
     }

    export var createNode: <a>(left: Node<a>, value: a, right: Node<a>) => Node<a> = Impl.createNode; 
    export var createEmptyList: <a>() => List<a> = Impl.createEmptyList; 
}

export = Public;

One big advantage here is that the Public module can be an exact carve-out of Impl's functionality depending on exactly what you want to do.

@zpdDG4gta8XKpMCd
Copy link
Author

Such emulation solves only one half of the problem by hiding the internals. In your example the Public instances of both List and Node interfaces can be created outside of the module. Such instances are no use, because the integrity is violated when an instance from Public is used instead of an instance from Impl. I mean from the Public stand point hidden fields === absent fields whereas Impl calls for the hidden fields to be present.

@zpdDG4gta8XKpMCd
Copy link
Author

@mhegazy, @DanielRosenwasser, @RyanCavanaugh

guys, is this @internal marker official?

image

@mhegazy
Copy link
Contributor

mhegazy commented Dec 2, 2015

guys, is this @internal marker official?

no, it is an internal "hack", that we can remove with no warning. we are looking for ways to make this scenario supported. so stay tuned.

@SalihKARAHAN
Copy link

👍 we need it 😊

@mhegazy
Copy link
Contributor

mhegazy commented Feb 20, 2016

related: #5228

@gitowiec
Copy link

How is work in progress with it? Or is there any work around? I wanted to use Namespaces but I can't because ///<reference path="./anyFileName.ts"/> does not work if there are imports in anyFileName.ts :(
I would like to achieve private package members, but exportable to allow file splitting inside of package.

@jonaskello
Copy link

I just found that flow now actually supports opaque types aliases which from what I understand is very similar to my suggestions in #15408 and #15465.

@lorenzodallavecchia
Copy link

A related case that I stumble upon quite often is when I want to provide a base class for extension and that class contains members that are only used for collaborating with other code inside the module.

// module utils.ts

export abstract class Base {
  exposedA() { };
  protected exposedB(): { };
  /* module */ modulePrivate(): { };
}

// other code that calls Base.modulePrivate

Currently, I have no way to do the above without enabling access to modulePrivate from other modules.

import { Base } from "./utils";

class Derived extends Base {
}

// here the internal details of the module are exposed via Derived.modulePrivate

The crucial point here is that I want to enable the extension of a class. In cases where I don't want others to extend I just implement the class internally (without export) and export an interface instead.

@ghost
Copy link

ghost commented Jan 28, 2019

No amount of (instance as any).privateField hackery can fix this. I propose a nice little internal modifier.

@jonaskello
Copy link

I recently found this article outlining how to approximate flow's opaque types in typescript. Doing what the article suggest this will solve this issue (module level privacy) but since typescript has no native support for opaque like flow does the solution is quite messy, especially for field level privacy.

@HarryGifford
Copy link
Member

This would be an extremely helpful addition to the language. It's one place where taking Javascript that was written using modules instead of classes becomes difficult to translate into Typescript. It would be great if it could cover opaque simple types like string and number too.

Some examples in the wild are the file descriptor in Node's fs.open function and a lot of the WebGL types, such as WebGLBuffer.

In my experience the reasons I've had for using modules and interfaces instead of classes are

  • Makes immutable style code easier, as you can de-structure objects and not lose their "class" type.
  • Easier to "tree-shake."
  • Can be serialized and deserialized without losing the class which is good when you want to interpret some data from localStorage, from a webworker or from a network request as an object of a particular type.
  • No need to privilege a single argument. Class methods have an implicit this argument, which is one reason it's hard to specify interfaces for them because the interface assumes that the this argument is implicit in each method in the interface. You have no way to specify the static methods that exist on that class. It's also unwieldy to define methods that take in multiple instances of that class or multiple ways to create the class.

I wonder if the ability to specify interfaces for modules could help here, perhaps using d.ts files to "seal" a module and selectively hide/show values and types?

@jonaskello
Copy link

jonaskello commented Jun 22, 2019

+1 for using modules over classes :-).

I wonder if the ability to specify interfaces for modules could help here, perhaps using d.ts files to "seal" a module and selectively hide/show values and types?

You can sort-of do this today using export. I sometimes do plugin-like design where I have several modules that expose the same "interface". Something like this:

plugin1.ts

const firstValue = 11;
export function foo(x: string): number {...}
export function bar(x: number): string {...}

plugin2.ts

const secondValue = 22;
export function foo(x: string): number {...}
export function bar(x: number): string {...}

main.ts

import * as Plugin1 from "./plugin1.ts"
import * as Plugin2 from "./plugin2.ts"

interface Plugin {
readonly foo: (x: string)=>number;
readonly bar: (x: number)=> string;
}

const plugins: ReadonlyArray<Plugin> = [Plugin1, Plugin2];

This will give errors if a module does not export according to the interface. However it is very implicit and having a more explicit way to model this would be nice.

I think having interfaces for modules would only cover exposing whole types or values? So in addition to that, to get field level privacy we need something like flow's opaque types. With that we can expose a type from a module without exposing the fields outside the module.

@HarryGifford
Copy link
Member

@jonaskello That's pretty nice. It certainly allows you to selectively hide the stuff you want to export. I think I should have clarified that I meant a signature and not necessarily an interface because they lack the ability to declare a type within them. I was thinking of something like

counter.ts

export type t = number;
export const make = () => 0;
export const incr = x => x + 1;
export const value = x => x;

and then I would like to be able to have a description like (not real typescript):

namespace /* or interface */ Counter {
   type t; // The implementation of this type is not exposed.
   make: () => t;
   incr: (x: t) => t;
   value: (x: t) => number;
}

I don't want users of this Counter to be aware of the fact that it's implemented using a number. At the same time, I don't want to be forced to use a class to get this kind of information hiding.

I suspect that allowing types to be declared inside of an interface would make type checking hard or impossible, but the ability to selectively hide parts of the module is really what I want.

If you're familiar with signatures (module types) from SML (Ocaml,) those are what I'm thinking of.

@pelotom
Copy link

pelotom commented Jul 1, 2019

First class support for existential types (#14466) might be another way to enable this kind of information hiding.

@jonaskello
Copy link

jonaskello commented Jul 2, 2019

@HarryGifford Yes that makes sense. I have only theoretical knowledge of ocaml/reasonml but I think you are refering the "Abstract Types" section of these docs. I think ocaml's module system looks really nice and if we could get that in typescript it would be great :-).

In ES modules you by design couple the module signature to the implementation by decorating the implementation with the export keyword. When you import * as foo there is an implicit interface formed by the exports which typescript infers (which was the point of my example above). I think the biggest problem with having a separate module signature is that not everyone will see it as a natural fit for ES modules as we need to decorate with export and keep that in sync with the signature type. I'm all for separating the module signature, just saying it might not be the view of the larger community.

So we are really discussing two things here, abstraction of types and separate module signatures. The abstraction of types is solved in flow by use of opaque types. Using opaque types in the counter example above would look like this:

counter.ts

export opaque type t = number;
export const make = (): t => 0;
export const incr = (x: t): t => x + 1;
export const value = (x: t): t => x;

I would rather have separate module signatures with abstract types like in ocaml, but I'm afraid that will be deemed too complex. The upside of opaque types are that they are very close to the original ES module design as they use the export keyword. So they seem like a more natural part of the language and are probably easier to implement.

@HarryGifford
Copy link
Member

HarryGifford commented Jul 2, 2019

@jonaskello Yes. Abstract types. The two major things missing from Typescript that exist in Ocaml are abstract types and functors (functions from modules to modules.) The first could be implemented with the opaque keyword as you describe. Functors would be really nice, but probably not realistic any time soon.

I think you're right that an opaque keyword is an easier sell. One thing that would change is that it would be very important to have type annotations on everything that is exported because the annotations can change the meaning of the code. For example, in counter.ts, value should read export const value = (x: t): number => x;. Otherwise you'd have no way to get the value of the counter once it's opaque.

@pelotom Are opaque types are a form of existential type? They allow you to define a set of operations on the type, without exposing the implementation of that type.

EDIT: Looks like #31894 is exactly such a proposal.

@pelotom
Copy link

pelotom commented Jul 2, 2019

@pelotom Are opaque types are a form of existential type? They allow you to define a set of operations on the type, without exposing the implementation of that type.

Yes: http://homepages.inf.ed.ac.uk/gdp/publications/Abstract_existential.pdf

EDIT: Looks like #31894 is exactly such a proposal.

Is it? It explicitly claims not to be related to existential types, and the primary use case seems to be about forward type declaration, not information hiding.

@HarryGifford
Copy link
Member

@pelotom You're right. I misread the proposal. Thrown off by the exists keyword.

@trusktr
Copy link

trusktr commented Dec 7, 2019

Hello, here's an idea for thought: #35554. It may be more verbose (syntax is up for bike shedding), but the concept is to explicitly specify which code has access to protected or private members.

Can that idea be expanded to pertain to various things, not just class members? For example, maybe a way to specify that an export is only available to certain other modules?

I too think it'd be neat allow access modifiers on regular types and object literals, and to be able to pass those types around (#35416).

@dyst5422
Copy link

Here's the workaround: Define the method using a symbol and don't export that symbol.

const GET_INTERNAL_SYMBOL = Symbol('GET_INTERNAL_SYMBOL');
export class SomeClass {
  private someThing: any;

  constructor(thing: any) {
    this.someThing = thing;
  }

  public get [GET_INTERNAL_SYMBOL]() {
    return this.someThing;
  }
}

function main() {
  const someClass = new SomeClass('the thing');
  const internalThing = someClass[GET_INTERNAL_SYMBOL];
}

Since you don't export the symbol, it's not accessible externally.

Downside is it still shows up.

@blexrob
Copy link

blexrob commented Apr 28, 2023

Another workaround is to create a new class variable and type that omit the unwanted properties:

type OmitClassProps<TypeOfT extends { new(...args: any): any }, S extends keyof InstanceType<TypeOfT> | keyof TypeOfT > = {
  new(...args: ConstructorParameters<TypeOfT>): Omit<InstanceType<TypeOfT>, S>;
} & Omit<TypeOfT, S>;

class MyClass {
  s: string;
  constructor(s: string) {
      this.s = s;
  }

  static a() { return 1; }
  static b() { return 2; }

  c() { return 3; }
  d() { return 4; }

}

/* Normally, a class is a class variable and the type of instances, both with the same name.
  Do the same for the filtered class 
*/  
export const FilteredClass: OmitClassProps<typeof MyClass, "b" | "d"> = MyClass;
export type FilteredClass = InstanceType<typeof FilteredClass>;

console.log(FilteredClass.a());
// @ts-expect-error -- Hidden
console.log(FilteredClass.b());

const filtered: FilteredClass = new FilteredClass("x");
console.log(filtered.c());
// @ts-expect-error -- Hidden
console.log(filtered.d());

@noppa
Copy link

noppa commented Dec 29, 2023

A bit verbose maybe (could be made shorter with some helpers), but you can do this pretty neatly nowadays with static blocks.

let getFoo: (instance: A) => string
let setFoo: (instance: A, value: string) => void

class A {
  private foo: string = ''
  static {
    getFoo = (instance: A) => instance.foo
    setFoo = (instance: A, value: string) => {
      instance.foo = value
    }
  }
}

const a = new A()

a.foo // Not allowed
getFoo(a) // Is allowed

Playground.
The same approach also works with "real" ES private fields.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests