Skip to content

Suggestion: making exports visible only to the same directory with JSDoc @package tag #41425

Description

@uhyo

Search Terms

JSDoc package directory package-private

Suggestion

If an export is annotated with @package, that export is visible only to files in the same directory.

I can think of two levels of @package support: a soft one would remove these exports from auto completion if not visible. A hard one would also emit a compile error.

See also: Use JSDoc: @package

Use Cases

We organize source code using directory structure. We often make exports that are meant to be referenced only by sibling modules. However, JavaScript/TypeScript has no idea of scoping based on file system, so we are free to import such "local" exports everywhere. TypeScript could help us do file system-based scoping.

Examples

// ----- src/foo/bar.ts
/**
 * @package
 */
export fooBar = "fooBar";

// ----- src/foo/index.ts
// Can import fooBar from "./bar"
import { fooBar } from "./bar";

// ----- index.ts
// CANNOT import fooBar from "./foo/bar"
import { fooBar } from "./foo/bar";

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.

Activity

  1. shrinktofit commented on Nov 8, 2020

    @shrinktofit

    I'd recommend configurating these package entries in tsconfig.

  2. uhyo commented on Jun 28, 2021

    @uhyo
    ContributorAuthor

    Just FYI, I created a userland solution on this as a combination of ESLint plugin and TypeScript Language Service Plugin.

    https://github.com/uhyo/eslint-plugin-import-access

    However, the implementation of Language Service Plugin is kind of barbarous, so support from TypeScript is still wanted.

  3. OzelotVanilla commented on Oct 28, 2022

    @OzelotVanilla

    I have another suggestion to this: use modifier before export to set the visibility. By default, export is public export that every file can see this.

    For example, if you want to export some function only inside of the directory (package), use inpackage export:

    For file src/sort/sort.ts:

    export function sort(/* some args here */) {  }
    inpackage export function quickSortPartition(/* */) {  }

    For file src/sort/ordered_stat.ts:

    import { quickSortPartition } from "./sort.ts"
    
    function findNthSmallest(/* */)
    {
        // Can use partition function here
        quickSortPartition()
    }

    But for other file src/test.ts:

    // Cannot import here
    import { quickSortPartition } from "./sort/sort.ts"
  4. A-Shleifman commented on Nov 29, 2022

    @A-Shleifman

    I've been thinking about this quite a bit recently.

    I like the idea of having a prefix for export declarations but:

    1. inpackage - I find it a bit too cumbersome
    2. package - unlike Java, where we explicitly declare packages, we don't have packages in JS. Actually, we do, but those are npm packages, so the term package might be confusing for pure JS developers.
    3. I was thinking about private - personally I like the idea of private export const .... Private class members are only accessible within the same class, while private exports could only be accessible within their directory. But I'm sure some people will be against using private for exports. The other problem - what if we want to make the export available one dir or two dirs above the current one? We could have a types folder which should be accessible by the parent and all children. Something like private(../..) export definitely doesn't look right.

    All of this led me to create a plugin (inspired by uhyo (@uhyo)'s plugin) that adds support for @scope ../.. comments that define scopes for exports.

    I wanted to create a TS decorator, that would make something like this possible:

    @scope("../..")
    export default "";

    but, unfortunately, TS doesn't support export decorators. If TS allows it then at least we would be able to create ESLint/TS plugins that would read the decorators instead of comments (which look quite a bit uglier)

  5. HQ6968 commented on Aug 25, 2023

    @HQ6968

    Suggestion : can use dir named "internal" , this dir can be import same level dir file. same as golang internal dir rule !

  6. decoded4620 commented on Oct 1, 2023

    @decoded4620

    Alex (@A-Shleifman) I think TS does support decorators, doesn't it? https://www.typescriptlang.org/docs/handbook/decorators.html Unless the terminology is wrong and you mean @annotations? Either way, I'm pretty sure both are supported in TS at this point, no?

  7. A-Shleifman commented on Oct 3, 2023

    @A-Shleifman

    Bow Archer (@decoded4620), I was talking about export decorators meaning decorators that go before export statements, which are not supported by TS.

  8. azerum commented on Dec 30, 2023

    @azerum

    I was thinking about idea similar to one of HQ6968. Here is list of my ideas/opinions. I've left questions that probably can be answered by somebody with more experience

    The idea

    1. Code can be grouped into folders (for the lack of better term I will literally call these "folders"), which have two parts: private and public. As an initial idea, let's say all the private code goes into private/ subfolder, and the rest of code is public:
    src/
      foo/
        private/
          foo-private.ts
        foo-public.ts
        bar/
          foo-bar-public.ts
      bar
        bar.ts
    

    So code in module bar.ts can import foo/foo-public.ts, foo/bar/foo-bar-public.ts, but not foo/private/foo-private.ts

    1. Modules inside private/ folders should be excluded from auto complete, unless we are typing code inside their folder.
    • When typing inside bar/bar.ts, we will never see anything from foo/private/*
    • However, when typing inside foo/*, we will see everything from foo/private/*

    Scope is given to the entire module, not to individual identifiers

    • Comparing to this proposal and @scope() suggested above, this approach makes everything within a module (i.e. file) public or private

    In my opinion, giving scope to individual functions/variables is harder to reason about: to see the entire public API of a component, or entire private API within the component, you would have to browse each individual file, since each file might contain a mix of both

    Is there ever a good case to mix public and private things in one module? Can we just split the code in multiple modules for such cases?

    Dependencies between folders

    It might be useful to declare dependencies between folders, instead of allowing every folder to import any other folder

    • In my opinion, this should be done explicitly at the top of the folder, so you can see what folders does the folder depend on, without having to browse its code

    I like how TS Project references do that: each folder has tsconfig.json at its top, with references properties declaring dependencies on other folder. What if TS simply allowed imports from folders listed in references, but not from their private/ subfolders?

    NPM dependencies are shared for all folders

    I think it's convenient that all such folder would be able to import any NPM packages from the root "package.json". If you want different components of code not to share each other's NPM dependencies, you can use a monorepo (e.g. Rush.js)

    Nested folders

    What if a folder contains another folder? Can the parent folder access private/ of the child folder? Can the child access the private/ folder of parent? We will need to think about it

    Child accessing things of parent

    I like idea of #41316, which introduces protected access level - things in parent visible to child. We could have protected/ folder for it

    Parent accessing things of child

    @scope(../..) allows to declare function/variable accessible from parent folder inside a child folder. In my opinion, folders should be "encapsulated": they can access the surrounding code (e.g. protected things of parent), but the surrounding code should not be able to access internals of component inside

    No barrel files

    • Comparing to packlets, we don't have to maintain index.ts files

    IMHO, barrel files are tiresome to maintain. It might be a good idea to have all public API declared in one file, but is there practical problem of browsing all folders except private/ to understand the public API of a component?

  9. A-Shleifman commented on Jan 24, 2024

    @A-Shleifman

    Is there ever a good case to mix public and private things in one module?

    Yes! This might not be the best example, but let's say we have a file defining a React Context for an encapsulated Component:

    const context = createContext({});
    
    export const ContextProvider = context.Provider; 
    // 👆 should only be accessible by the internal component files
    
    export const useComponentContext = () => useContext(context); 
    // 👆 should be accessible by any component anywhere wrapped in the Component providing the context

    but the surrounding code should not be able to access internals of component inside

    Generally, that's true. That's why we're having this conversation, but there are exceptions. The most basic would be an index file re-exporting an export from a child folder.

    utils
    ├── schema-utils/
    │   ├── parsers.ts
    │   └── validators.ts
    └── index.ts

    In this case, it should be possible for the index.ts file to import from schema-utils/validators.ts for example. I have a big project with a few of those exceptions even outside index files.

  10. A-Shleifman commented on Jan 24, 2024

    @A-Shleifman

    I would love a solution to this problem to be integrated into TS, but while we wait, here's a v2 of eslint-plugin-export-scope

  11. ildede commented on Oct 24, 2024

    @ildede

    Personally, I would use this kind of export primarly for testing purpose: Exporting a "private" function that could be seen only by the associated test file.

  12. JoaoSoaresDev commented on Dec 3, 2024

    @JoaoSoaresDev

    This would be a great addition to TS. For me, it would allow me to better break it down my file structure ensuring that modules are self-contained. For e.g., in my React projects, I would have my folder structure like:

    src/
      components/
        button/
          index.tsx // contains the TSX
          style.ts // styles are only exported internally, so no other modules outside button/ can access these styles
          validation.ts // same here, can't be access outside
          interface.ts // same here, can't be access outside
    

    If we had something like the internal export that was presented here, this would ensure other components wouldn't use the styles or validation that are only for the button component. In that way I don't need to keep everything within the same file.

    This would also allow exporting two modules with same name in different scopes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions