ES2015 module syntax is preferred over custom TypeScript modules and namespaces @typescript-eslint/no-namespace

Viewed 42114

I'm receiving the following error running npm start:

ES2015 module syntax is preferred over custom TypeScript modules and namespaces @typescript-eslint/no-namespace

    namespace InternalThings {...}

I tried to research this but it's very confusing.

Why does this is happening? How to fix it?

I tried to put some flags on my tsconfig.json but so far no success;

4 Answers

This is a lint error, caused by this lint rule: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-namespace.md

If you find the rule useful and want to keep it, then you'll need to modify your code to use import and export instead of namespace. See the documentation of the rule for what counts as a fix.

If you like the rule, but want to disable the rule for this line, add the following just above it:

// eslint-disable-next-line @typescript-eslint/no-namespace

If you don't like the rule and want to disable it entirely, then edit your .eslintrc file to have the following line:

rules: {
  "@typescript-eslint/no-namespace": "off"
}

To fix this error, instead of:

export namespace InternalThings {
    export function myFunction() {
    }

    export class MyClass {
    }
}
import { InternalThings } from './internal-things';

InternalThings.myFunction();

you expose all the members of the namespace directly:

export function myFunction() {
}

export class MyClass {
}

and you import it like this:

import * as InternalThings from './internal-things';

InternalThings.myFunction();

The main idea is that users of your module can import only what they want, or name your module differently:

import * as CustomModuleName from './internal-things';

CustomModuleName.myFunction();
import { MyClass } from './internal-things';

let field = new MyClass();

Fix Lint Error While Maintaining Same API

If you would like to handle the lint error without breaking any current implementations you can do the following, but you should really look at the above answer before committing to this: https://stackoverflow.com/a/63574739/349659

Before

Implementation

export namespace Container {
  export function someCall() { }
  export function anotherCall() { }
}

Consumer

import { Container } from './Container'

Container.someCall()
Container.anotherCall()

After

Option 1

// These are essentially private
function someCall() { }
function anotherCall() { }

// We expose them here
// This feels like a step towards CommonJS, but is valid ES Module code
export const Container = {
  someCall,
  anotherCall,
}

Option 2

You could also define and encapsulate the function calls directly into the object as well like so:

export const Container = {
  someCall() {},
  anotherCall() {},
}

In Conclusion

If you have a large codebase and want to "quickly" appease your linter, you can do a refactor like above. Make sure to consider this answer https://stackoverflow.com/a/63574739/349659 and the reasoning behind it.

At the end of the day, the quickest fix that requires no code change is to simply turn off this linting rule as mentioned in this answer: https://stackoverflow.com/a/58271234/349659

If you're starting from scratch and run into this issue I would consider utilizing the modern implementation as the linter hints towards, but you may find that you enjoy namespaces and simply want them as well. If you're part of a team you may want to get their feedback first and and follow a team standard.


Edge Cases & Considerations

One case I've ran into is having multiple namespaces in the same file. In this scenario you may then have name collisions after removing the namespace.

Example

Before

export namespace Container {
  export function someCall() { }
  export function anotherCall() { }
}

export namespace AnotherContainer {
  export function someCall() { }
  export function anotherCall() { }
}

After

Renaming Collisions

In this scenario when you remove the namespace you can rename the collisions while maintaining the export like so:

function containerSomeCall() { }
function containerAnotherCall() { }

export const Container = {
  someCall: containerSomeCall,
  anotherCall: containerAnotherCall,
}

function anotherContainerSomeCall() { }
function anotherContainerAnotherCall() { }

export const AnotherContainer = {
  someCall: anotherContainerSomeCall,
  anotherCall: anotherContainerAnotherCall,
}
Decoupling the Code

Another option is to decouple them into their own files. If you want to maintain the exports of the original file though you will need to import and expose them which may seem duplicate, but may be an intermittent step towards a larger refactoring (later updating imports to point at the new files). This also allows you to start writing more modern ESM code too if you would like, while proxying new exports through the old module.

Container.ts

function someCall() { }
function anotherCall() { }

export const Container = {
  someCall,
  anotherCall,
}

AnotherContainer.ts

function someCall() { }
function anotherCall() { }

export const AnotherContainer = {
  someCall,
  anotherCall,
}

OriginalFile.ts

export * from './Container'
export * from './AnotherContainer'

We can proxy the new ESM modules through the old original module.

Related