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

Feature Request: Modules Packages #35688

Closed
5 tasks done
DanTsk opened this issue Dec 15, 2019 · 2 comments
Closed
5 tasks done

Feature Request: Modules Packages #35688

DanTsk opened this issue Dec 15, 2019 · 2 comments
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript

Comments

@DanTsk
Copy link

DanTsk commented Dec 15, 2019

Search Terms

package, modules encapsulation, modules union, modularization, modules categorization, module access level

Suggestion

Provide a mechanism for explicit modularization with strict boundaries and access levels within a single project. This suggestion is only about type - checking and won't affect runtime behaviour.

Applications and teams grow really fast nowadays, so a convenient way of encapsulation is required. The main point is to categorize application modules and provide access protection for them. Currently, we have access protection control on a single module (file) level using export modifier that sets the access level globally in the whole project. But it's a common situation when module/package can consist of more than one file, so to call some internal implementations from another file, we need to export them (make them globally accessible) breaking access hierarchy.

Current approaches and workarounds:

  1. Namespaces
    Namespaces can't be used to solve this problem, as primarily were developed with the global context in mind. But for now, almost everyone and everything (frameworks, build tools, environments, etc.) focused on modules. Due to current namespaces behaviour, it's more about types declarations. (More info)
    In my personal opinion, even modification of current namespaces behaviour won't solve the problem, as actual modules merging is not TypeScript responsibility. By the way, such change will also affect runtime behaviour that isn't really good. And another thing is syntax. Looks like TypeScript namespaces were heavily inspired by C# namespaces. For TypeScript/JavaScript, It's not really good to have structures that wrap top-level (file) code, as it kills top-level at all.

  2. Project References
    Project References provide some kind of module categorization and can be as alternative for mono-repos(in some way). It's a good approach for relatively large units, but not really good for small packages consisting of only 2-3 modules (files). By the way, Project References can't solve access protection problems, as all referenced modules will be public by default. Also, Project References affects build behaviour, so frameworks and tools must adjust their processes.

  3. Index File
    The most popular workaround is exporting package members using index.ts file. Such approach categorizes modules within a single project, but it's more like a visual fix. Access to internal members still opened.

Use Cases

  1. Modules categorization
    Application components can be categorized by their type. For example - models, views, controllers in MVC. It will provide more strict architectural boundaries.

  2. Internal implementations
    It's a common situation when a single application component consists of several internal application components, that don't need to be publicly accessible.
    The best example is Atomic Design in React Apps. Imagine that we are building some complex React component, that aggregates smaller components inside and adds some new logic (functions, etc.). Due to component complexity, we can't fit everything into a single file (it's semantically incorrect), so we decompose it in small modules with different responsibilities. But we just need the main component publicly accessible. Other internal things will simply clog the project with publicly accessible units that won't be used anywhere except for internal implementation.

  3. Private Access Level
    Another thing is primarily about access boundaries. For example, a class with a private constructor and static factory methods. Or private inner class that used only for internal logic. Sometimes developers don't want to provide full (uncontrolled) access to all units but want to provide only a few controlled entry points. Same thing with modules. Several modules can depend on another module that used only within these modules and doesn't need to be publicly accessible, so it's semantically private. The main points - are granular access level control within project and encapsulation.

  4. Performance optimization
    Based on packages resolution, TypeScript Language Services can skip a lot of work and provide type-checking,modules resolution, etc. only within package/global boundaries.

Examples

package and using keywords can be used for this functionality.
That's enough to have only 2 statements in simplified version:

  1. package <identifier>; - inclue module in package
  2. using <identifier, identifier, ...>; - reference packge(s) from global context or from another package

For example:
Defining that module is part of package

package Foo;
//...imports

Referencing package from global package/context module

using Foo;
//...imports

Referencing package from of another package module

package Bar;
using Foo;
//...imports

Behaviour description

  1. Modules in packages don't need any additional statements to reference global context modules.
  2. Module can be included only in one package, but can have many packages references.
  3. Referenced packages can be accessed only in referencing module scope.
  4. Package-referencing don't replace imports functionality. Imports still required. When imported module included in some package, module resolution must fail if there is no package-reference in importer file.
// some.ts
package Bar;

export function foo() { ... }
// other.ts
using Bar;
// Module resolution won't fail, as `some.ts` package Bar is referenced
import { foo } from "./some";

foo();

In this case, we will have entry-point files to packages that are part of global context and used only for package referencing and exporting package members.

// bar/some.ts
package Bar;

export function foo() { ... }

export function baz() { ... }
// bar/another.ts
package Bar;

export function hello() { ... }
// bar/index.ts
using Bar;

export { foo } from "./some.ts";
export { hello } from "./another.ts";

foo function can be imported from ./bar/index.ts, but baz function can't be imported directly from bar/some.ts, as package referencing is not transitive. Referenced packages can be accessed only in referencing module scope

// app.ts
import { foo, hello } from "./bar";

foo();
hello();

Access level modifiers

Only one access level modifier is needed - protected. So export declaration must be modified in the following way - export protected? Statement/Declaration. (Added optional modifier protected). Protected exports can be used within package, and can't be imported anywhere outside package.

// some.ts
package Bar;

export function foo() { ... }

export protected function baz() { ... }
// another.ts
package Bar;
// Everything is okay, as this module is part of 'Bar' package
import { baz } from "./some.ts"; 

export function hello() {
    baz();
    ...
}
// index.ts
using Bar;
// Error, as this module is referencing 'Bar' package, but is not a part of it
import { foo, baz } from "./some.ts"; // 'baz' member is accessible only within 'Bar' package
import { hello } from "./another.ts";

...

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@DanTsk DanTsk changed the title Modules Packages [Feature Request] Modules Packages Dec 15, 2019
@DanTsk DanTsk changed the title [Feature Request] Modules Packages Feature Request: Modules Packages Dec 15, 2019
@RyanCavanaugh RyanCavanaugh added Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript labels Dec 19, 2019
@RyanCavanaugh
Copy link
Member

I think the best reading of this is trying to solve the same problems as the package-level internal modifier - #321

As for making new units of organization through declarations, it's really quite far out of scope for us since it doesn't relate to the types of values or an existing visibility construct. It seems like a task that could really be implemented quite well with a lint-style tool since all the enforcement is purely syntactic. You'd need some comment-based DSL, but we don't want to add non-annotation statements to the language anyway.

@phaux
Copy link

phaux commented Feb 29, 2020

I don't know if TypeScript supports it already but this is something that package exports solve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants