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;
}