80

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;

6 Answers 6

87

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

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"
}
Sign up to request clarification or add additional context in comments.

2 Comments

can you provide the fix you see from the documentation?
Ignoring a rule is not the best solution. Fix it via correct import.
55

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();

1 Comment

Doesn't that break auto import? If it is exported as namespace, I can type the namespace name and have it adds the import for me. If types are exported directly, I have to add the import by hand.
15

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.

4 Comments

Perhaps this needs a new question - however it's the same error, and - I think - you have the answer: How would you implement the above in this namespace with naming collisions? declare global { namespace Express { export interface Request { reqId: string; } } }
@redevill you may want to open up a new question with greater context. You're looking to keep it like: Express.Request? I'm not certain if you can encapsulate types like that without name spaces. Unless Request is exported top-level and then you would have to import * as Express from './myExpress' I have concerns about your naming conflicts and what you're aiming to accomplish exactly.
Motivation comes from the accepted augmentation for express.request interface stackoverflow.com/a/47448486/452928. When implementing this, you get the linting error mentioned by the OP.
@redevill since you're working with a library and their implementation is outside of your hands it might be best to utilize an ignore pattern/rule/comment in this scenario.
3

The error is coming from eslint. You have to either ignore '@typescript-eslint/no-namespace' rule in the config or rewrite your code using ES6.

Custom TypeScript modules (module foo {}) and namespaces (namespace foo {}) are considered outdated ways to organize TypeScript code. ES2015 module syntax is now preferred (import/export)

Refer https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-namespace.md

Comments

3

About using name spaces today (2024):

The Idea of that rule is that because of ES modules you don't need names spaces as much as before. That is true, for all of the examples above. There is one exception tough. If you want to wrap types and interfaces in some object, then using a name space is your only option. For example:

// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace NameSpaceWithTypes {
  export type MyType = {...};
  export interface MyInterface {...}
}

Now, why would you use this? I use it if I have an app with tones of types, and some of them are "rare", so you can have a bit more "order", like this:

const someVariable : NameSpaceWithTypes.MyType = {...}

(the "order" thing is because you can't import MyType directly, you have to import the namespace)

but this is the only reason I can think of for using namespaces today.

1 Comment

Right this rule is stupid, namespaces are mainly used for type augmentation
-1

I personally find, the problem with the no-namespaces rule, is that it pushes clients (API consumers) into using the very footgun syntax:

import * as SomeGarbageName from './some/dir/UtilityMethods';

The problem with this is that in larger codebases, SomeGarbageName dramatically obscures what the code means when reading it, because everyplace it appears, people make different "short convenience names" for the import-wrapper. These names also don't get globally refactor-renamed like a namespace rename does when using tool-supported type-aware rename.

In a sense, I find the "mis-feature" that the clients can create a custom import name to be unmitigated disaster.

Therefore, my solution is to add an ESLINT rule which requires that the imported name match the filename, effectively enforcing that the local import name MUST match the module-filename. This allows the modern no-namespaces approach, without the towel-of-babylon random name inventing disaster.

import * as UtilityMethods from './some/dir/UtilityMethods';

I also turn off the no-namespace rule, and use namespaces when appropriate, as the namespace I would prefer to see does not always match the filename, for various reasons.

YMMV, but this is my preference for symbol name sanity.

/**
 * @fileoverview Enforces that namespace imports match the filename/directory being imported from
 * @description When using `import * as foo from './path/foo'`, ensures the namespace name matches the file/directory name
 * @author David Jeske with Claude Opus 4.1 as Kilo Code
 */

"use strict";

const path = require('path');

module.exports = {
  meta: {
    type: "suggestion",
    docs: {
      description: "Enforces that namespace imports match the filename or directory name being imported from",
      category: "Best Practices",
      recommended: true,
    },
    fixable: "code",
    schema: [
      {
        type: "object",
        properties: {
          ignoreCase: {
            type: "boolean",
            default: false
          },
          allowIndexFiles: {
            type: "boolean", 
            default: true
          }
        },
        additionalProperties: false
      }
    ],
    messages: {
      mismatchedNamespace: "Namespace import '{{namespace}}' should match filename '{{expected}}' from '{{importPath}}'",
      indexFileHint: "Namespace import '{{namespace}}' from index file should match parent directory '{{expected}}'"
    }
  },

  create: function(context) {
    const options = context.options[0] || {};
    const ignoreCase = options.ignoreCase || false;
    const allowIndexFiles = options.allowIndexFiles !== false;

    return {
      ImportDeclaration(node) {
        // Only check namespace imports (import * as X from 'path')
        if (node.specifiers.length !== 1 || 
            node.specifiers[0].type !== 'ImportNamespaceSpecifier') {
          return;
        }

        const namespaceSpecifier = node.specifiers[0];
        const namespaceName = namespaceSpecifier.local.name;
        const importPath = node.source.value;

        // Skip node_modules and external packages
        if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
          return;
        }

        // Parse the import path to get the filename/directory name
        const pathParts = importPath.split('/');
        let expectedName = pathParts[pathParts.length - 1];

        // Remove file extensions
        expectedName = expectedName.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, '');

        // Handle index files - use parent directory name
        if (allowIndexFiles && expectedName === 'index') {
          if (pathParts.length > 1) {
            expectedName = pathParts[pathParts.length - 2];
            
            // Report with special message for index files
            if (!compareNames(namespaceName, expectedName, ignoreCase)) {
              context.report({
                node: namespaceSpecifier,
                messageId: "indexFileHint",
                data: {
                  namespace: namespaceName,
                  expected: expectedName
                },
                fix(fixer) {
                  return fixer.replaceText(namespaceSpecifier.local, expectedName);
                }
              });
            }
            return;
          }
        }

        // Check if namespace matches the expected name
        if (!compareNames(namespaceName, expectedName, ignoreCase)) {
          context.report({
            node: namespaceSpecifier,
            messageId: "mismatchedNamespace",
            data: {
              namespace: namespaceName,
              expected: expectedName,
              importPath: importPath
            },
            fix(fixer) {
              return fixer.replaceText(namespaceSpecifier.local, expectedName);
            }
          });
        }
      }
    };
  }
};

/**
 * Compare two names with optional case-insensitive comparison
 */
function compareNames(name1, name2, ignoreCase) {
  if (ignoreCase) {
    return name1.toLowerCase() === name2.toLowerCase();
  }
  return name1 === name2;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.