I want to bring modularity into my typescript application and build different modules. Inside the modules I want to hide classes that should not be used outside the module. I currently have one ts-file for every class, which requires me to export every class and therefore does not hide anything. I understand that each typescript file is one module, meaning I could just throw all classes of a module in one file and just export the relevant classes. Unexported classes would be hidden, as they are only accessable within the file. This would, however, greatly decrease the readabily and editability of my application and would not be worth for me.
I read about the concept of barrel files and did some testing myself. This is how I picture the simplefied barrel pattern:
Filepath
/myModule1/
barrel.ts
MyHiddenClass.ts
MyExportedClass.ts
/myModule2/
MyOtherClass.ts
MyHiddenClass.ts
export class MyHiddenClass {
// some utilities that are only supposed to be used in myModule1
// export is still needed, so that files in this module can access this file
}
myExportedClass.ts
import { MyHiddenClass } from "./MyHiddenClass";
export class MyExportedClass {
// do something with MyHiddenClass
// use me in myModule2
}
barrel.ts
// only export the classes, that are supposed to be used in other modules
export * from './myExportedClass';
MyOtherClass.ts:
// do not import directly from the files, only use the barrel file
import { MyExportedClass } from '../myModule1/barrel'
export class MyOtherClass {
// do something with MyExportedClass
}
This works fine, however, this would also still work from another module:
MyOtherClass.ts:
import { MyExportedClass } from "../myModule1/MyExportedClass";
import { MyHiddenClass } from "../myModule1/myHiddenClass";
export class MyOtherClass {
// do something with MyExportedClass
// do something that is not intended with MyHiddenClass
}
So it is possible to completely ignore the barrel file and directly import all files. As a matter of fact, when working with Visual Studio Code's autocomplete, the direct import is prioritized over the barrel file. Which makes it extremely easy to miss the barrel file completely and ignore the intended modularity. I feel like, I could just insert a readme file into the module instead, which politely asks to only import the intended classes and it would have the same effect.
My question is therefore, wether there is a possibility to have every class in a seperate ts-file and still make use of the typescript modularity? Maybe there is a way to force the use of the barrel file or maybe it is somehow possible to interpret a directory as a module. Or let me know if I am missing something and my approach does not make sense.
Thanks in advance
As some commentors have suggested I started using ESLint for Typescript. I tried around with the rules from the module plugin for eslint, but they did not seem to work properly, so I am now using the standard rule no-restricted-imports. I restructured my project into a tree structure, like this:
/src
/modules
/myModule1
module.ts
/myModule1
MyHiddenClass.ts
MyExportedClass.ts
MyExportedClass2.ts
/myModule2
module.ts
/myModule2
MyOtherClass.ts
Each module has a subdirectory of the modules directory, containing a barrel file called module.ts (name of the file does not matter) and another subdirectory with the class implementations. In the implementations subdirectory each file contains an exported class. The module.ts looks like this (Module internal classes like MyHiddenClass are obviously not included):
export { MyExportedClass} from "./myModule1/MyExportedClass.ts";
export { MyExportedClass2} from "./myModule1/MyExportedClass2.ts";
No the trick to force the usage of the module.ts files is this rule implementation in the eslint config file:
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
"*/modules/*/*/*",
"../*"
]
}
]
}
The first rule prevents imports from the implementation subdirectories:
//pattern * /modules/ * / * / *
//does not match: src/modules/myModule1/module.ts
//does match: src/modules/myModule1/myModule1/MyHiddenClass.ts
The second rule prevents relative imports beteen modules that would sidestep the first rule like this import from MyOtherClass.ts:
import { MyHiddenClass} from "../../myModule1/myModule1/MyHiddenClass.ts";
Instead my imports look like this:
import { MyExportedClass, MyExportedClass2 } from "src/modules/myModule1/module.ts";
MyHiddenClass is therefore essentially hidden from other modules. It also works very will with the ESLint extension for Visual Studio Code. It automatically generates the import pattern and avoids patterns that are forbidden by the eslint config. Also classes that are hidden from the current module do not show up in autocomplete as there is no legal way to import the files.