One feller's views on the state of everyday computer science & its application (and now, OTHER STUFF) who isn't rich enough to shell out for www.myfreakinfirst-andlast-name.com
Using 89% of the same design the blog had in 2001.
FOR ENTERTAINMENT PURPOSES ONLY!!!
Back-up your data and, when you bike,
always wear white.
As an Amazon Associate, I earn from qualifying purchases. Affiliate links in green.
I noticed today I've been using TypeScript for years and really haven't blogged about it at all. I guess that's not surprising; it essentially does exactly what it says it does: Turns the Wild West of fully dynamic JavaScript development into a strongly-typed, well-controlled arena.
I love doing dynamic stuff with JavaScript (a dynamic language provides for a very particular set of mental challenges), but for the most part, aside from massaging payloads to and from servers (so when we jump concerns from, eg, client to API), it's A Very Bad Idea to go full dynamic. And that means I love TypeScript. It takes some of the silly putty of JavaScript and puts it into the hard Lego blocks of C#, if I can reuse an interview answer I gave once upon a time...
The use case
But as I prepare a large and reasonably mature .NET MVC/AngularJS codebase [sic] for TypeScript, I ran back into the age old issue of how to expose my exports. Is there a way to import an entire directory of files easily? No, not really, no there's not.
The usual shorthand for doing this is to import from barrel files. And that's probably the way to go to start, creating multiple child barrels per folder and exposing them in an uber-barrel at the top. We have scores of AngularJS files to move to TypeScript, and currently have a <script> import per file (no, literally a line of <script> per AngularJS source file) in the main cshtml page, all generated by a giant .NET MVC's bundle config. Ewww. So we cut each folder into its own import and import the parent barrel in the index.html equivalent.
(I'd mistakenly thought the bundles were log files, I think as in logrolling, another term I tend to remember in concept but forget the term, though with the growing importance of elections, I'm doing better remembering now.)
You can instead add a barrelย demo/index.tsย containing the following:
// demo/index.tsexport*from'./foo';// re-export all of its exportsexport*from'./bar';// re-export all of its exportsexport*from'./baz';// re-export all of its exports
Now the consumer can import what it needs from the barrel:
import{Foo,Bar,Baz}from'../demo';// demo/index.ts is implied
Since we are using many components in app.routing.module.ts file, we must import their reference for .component.ts and .module.ts files. A general import statements would looks like this:
import { DashboardComponent } from './dashboard/dashboard.component';
import { LoginComponent } from './login/login.component';
import { SignUpComponent } from './sign-up/sign-up.component';
import { AccountOverviewComponent } from './dashboard/account/account-overview/account-overview.component';
import { AccountSecurityComponent } from './dashboard/account/account-security/account-security.component';
import { CartOverviewComponent } from './dashboard/cart/cart-overview/cart-overview.component';
import { CartItemDetailsComponent } from './dashboard/cart/cart-item-details/cart-item-details.component';
import { CartComponent } from './dashboard/cart/cart/cart.component';
import { ProductComponent } from './dashboard/products/product/product.component';
import { ProductsDetailsComponent } from './dashboard/products/products-details/products-details.component';
import { ProductsOverviewComponent } from './dashboard/products/products-overview/products-overview.component';
import { DashboardModule } from './dashboard/dashboard.module';
import { AccountModule } from './dashboard/account/account.module';
import { CartModule } from './dashboard/cart/cart.module';
import { LoginModule } from './login/login.module';
import { SignUpModule } from './sign-up/sign-up.module';
import { ProductsModule } from './dashboard/products/products.module';
...
Typescript module resolution picks up index.ts file from folder name if it is there and try to import packages.
In this file, we can export all folder specific components and modules. For example, consider index.ts file inside products folder:
export * from './product/product.component';
export * from './products-details/products-details.component';
export * from './products-overview/products-overview.component';
export * from './products.module';
...
Here are the complete import statement[s] using index.ts file approach
import { AccountOverviewComponent, AccountSecurityComponent, AccountModule } from './dashboard/account';
import { CartComponent, CartOverviewComponent, CartItemDetailsComponent, CartModule } from './dashboard/cart';
import { ProductComponent, ProductsDetailsComponent, ProductsOverviewComponent, ProductsModule } from './dashboard/products';
import { DashboardComponent, DashboardModule } from './dashboard';
import { LoginComponent, LoginModule } from './login'
import { SignUpModule, SignUpComponent } from './sign-up';
Looks neat and simple, [doesn't] it?
The evil side of barrels: Circular dependencies
As I got ready to put our AngularJS files into barrels for the TypeScript port, I recalled that thar be dragons in the barrel file. But I couldn't remember what it was, precisely.
Turns out it's that importing and exporting indiscriminately can run you into circular dependencies.
Although its is not directly related to barrel files i-e circular dependency. Circular dependency occurs when a Module A somehow imports itself.
Make sure you donโt get into this kind of trouble especially when using Barrel files.
And because everyone and their brother says circular dependencies, but don't actually explain where it'd happen, here's one real-world example of this issue:
A circular dependency might also be caused when using "barrel files"/index.ts files to group imports. Barrel files should be omitted when it comes to module/provider classes. For example, barrel files should not be used when importing files within the same directory as the barrel file, i.e.ย cats/cats.controllerย should not importย catsย to import theย cats/cats.serviceย file. For more details please also seeย this github issue.
So in the end, nothing has changed... I didn't remove/change the code. I've just moved 2 functions from separate files to the test file body. That's it!
And of course, the imports in the test file changed:
/* <...> */import{createAccessToken,createAccount,createUser,// findOneInvitation, <-- was here when I had an exception// findOneAccount, <-- was here when I had an exceptionE2ETestingContainer,E2ETestingModulesFactory,findOneRole,findOneUser,FixtureLoader,FixtureLoaderConfig,JEST_E2E_TIMEOUT,MongooseFixturesService,}from'../../testing';/* <...> */
I would suggest omitting barrel files when it comes to module's providers/module class OR using them very carefully (for example, even if you create a barrel, you shouldn't use it from within the same module itself [only from outside] and when circular dependency may potentially appear).
Wait, huh?
I'm not going to swear I get it. I think there might be some interaction with dependency injection.
See this blog post on class declarations not hoisting in JavaScript and how order matters. I think a similar issue is illustrated (as in literally illustrated with a flowchart) here, but doesn't mention barrels.
See also this reddit post [sic] that lays out a circular dependency setup for related objects reasonably well.
An option to ignore these kind of circular deps A => barrel => A
I think that's it in a nutshell. Something in the indiscriminately broad barrel is importing/exporting the same thing that started the chain. I'm suspicious it's actually A => barrel => Lots of difficult to follow dependencies => A, but you get the idea. If you import a barrel that could contain something used in the same library, you can easily get a circular dependency. Instead, you should roll the barrel in one directly only, never using it within your own self-contained module, only exposing its contents to use in another app that imports it.
And that makes sense with the original example of having helper functions that blow up. If your object imports helper functions and models that also use the helper functions you could be entering a world of pain. Though I'm still not 100% convinced. More like 50% sure I smell something.
@lasimoneย just donโt use barrels within same module. Use relative imports.
Wait a minute. Didn't the Angular.io style guide used to encourage barrels?
Yes, yes it did. But though it originally encouraged them, the Angular style guide now doesn't mention barrels. This change used to be described on the Angular changelog page, but isn't there any more.
StyleGuideย explains recommended conventions for Angular modules (NgModule). Barrels now are far less useful and have been removed from the style guide; they remain valuable but are not a matter of Angular style.
The postings on this site are [usually] my own and do not necessarily reflect the views of any employer, past or present, or other entity. About Our Author