no-extraneous-class
Disallow classes used as namespaces.
Extending "plugin:@typescript-eslint/strict"
in an ESLint configuration enables this rule.
This rule reports when a class has no non-static members, such as for a class used exclusively as a static namespace.
Users who come from a OOP paradigm may wrap their utility functions in an extra class, instead of putting them at the top level of an ECMAScript module. Doing so is generally unnecessary in JavaScript and TypeScript projects.
- Wrapper classes add extra cognitive complexity to code without adding any structural improvements
- Whatever would be put on them, such as utility functions, are already organized by virtue of being in a module.
- As an alternative, you can
import * as ...
the module to get all of them in a single object.
- IDEs can't provide as good suggestions for static class or namespace imported properties when you start typing property names
- It's more difficult to statically analyze code for unused variables, etc. when they're all on the class (see: Finding dead code (and dead types) in TypeScript).
This rule also reports classes that have only a constructor and no fields. Those classes can generally be replaced with a standalone function.
module.exports = {
"rules": {
"@typescript-eslint/no-extraneous-class": "error"
}
};
Try this rule in the playground ↗
Examples
- ❌ Incorrect
- ✅ Correct
class StaticConstants {
static readonly version = 42;
static isProduction() {
return process.env.NODE_ENV === 'production';
}
}
class HelloWorldLogger {
constructor() {
console.log('Hello, world!');
}
}
abstract class Foo {}
Open in Playgroundexport const version = 42;
export function isProduction() {
return process.env.NODE_ENV === 'production';
}
function logHelloWorld() {
console.log('Hello, world!');
}
abstract class Foo {
abstract prop: string;
}
Open in PlaygroundAlternatives
Individual Exports (Recommended)
Instead of using a static utility class we recommend you individually export the utilities from your module.
- ❌ Incorrect
- ✅ Correct
export class Utilities {
static util1() {
return Utilities.util3();
}
static util2() {
/* ... */
}
static util3() {
/* ... */
}
}
Open in Playgroundexport function util1() {
return util3();
}
export function util2() {
/* ... */
}
export function util3() {
/* ... */
}
Open in PlaygroundNamespace Imports (Not Recommended)
If you strongly prefer to have all constructs from a module available as properties of a single object, you can import * as
the module.
This is known as a "namespace import".
Namespace imports are sometimes preferable because they keep all properties nested and don't need to be changed as you start or stop using various properties from the module.
However, namespace imports are impacted by these downsides:
- They also don't play as well with tree shaking in modern bundlers
- They require a name prefix before each property's usage
- ❌ Incorrect
- ⚠️ Namespace Imports
- ✅ Standalone Imports
// utilities.ts
export class Utilities {
static sayHello() {
console.log('Hello, world!');
}
}
// consumers.ts
import { Utilities } from './utilities';
Utilities.sayHello();
Open in Playground// utilities.ts
export function sayHello() {
console.log('Hello, world!');
}
// consumers.ts
import * as utilities from './utilities';
utilities.sayHello();
Open in Playground// utilities.ts
export function sayHello() {
console.log('Hello, world!');
}
// consumers.ts
import { sayHello } from './utilities';
sayHello();
Open in PlaygroundNotes on Mutating Variables
One case you need to be careful of is exporting mutable variables. While class properties can be mutated externally, exported variables are always constant. This means that importers can only ever read the first value they are assigned and cannot write to the variables.
Needing to write to an exported variable is very rare and is generally considered a code smell. If you do need it you can accomplish it using getter and setter functions:
- ❌ Incorrect
- ✅ Correct
export class Utilities {
static mutableCount = 1;
static incrementCount() {
Utilities.mutableCount += 1;
}
}
Open in Playgroundlet mutableCount = 1;
export function getMutableCount() {
return mutableField;
}
export function incrementCount() {
mutableField += 1;
}
Open in PlaygroundOptions
This rule accepts the following options:
type Options = [
{
/** Whether to allow extraneous classes that contain only a constructor. */
allowConstructorOnly?: boolean;
/** Whether to allow extraneous classes that have no body (i.e. are empty). */
allowEmpty?: boolean;
/** Whether to allow extraneous classes that only contain static members. */
allowStaticOnly?: boolean;
/** Whether to allow extraneous classes that include a decorator. */
allowWithDecorator?: boolean;
},
];
const defaultOptions: Options = [
{
allowConstructorOnly: false,
allowEmpty: false,
allowStaticOnly: false,
allowWithDecorator: false,
},
];
This rule normally bans classes that are empty (have no constructor or fields). The rule's options each add an exemption for a specific type of class.
allowConstructorOnly
allowConstructorOnly
adds an exemption for classes that have only a constructor and no fields.
- ❌ Incorrect
- ✅ Correct
class NoFields {}
Open in Playgroundclass NoFields {
constructor() {
console.log('Hello, world!');
}
}
Open in PlaygroundallowEmpty
The allowEmpty
option adds an exemption for classes that are entirely empty.
- ❌ Incorrect
- ✅ Correct
class NoFields {
constructor() {
console.log('Hello, world!');
}
}
Open in Playgroundclass NoFields {}
Open in PlaygroundallowStaticOnly
The allowStaticOnly
option adds an exemption for classes that only contain static members.
We strongly recommend against the allowStaticOnly
exemption.
It works against this rule's primary purpose of discouraging classes used only for static members.
- ❌ Incorrect
- ✅ Correct
class EmptyClass {}
Open in Playgroundclass NotEmptyClass {
static version = 42;
}
Open in PlaygroundallowWithDecorator
The allowWithDecorator
option adds an exemption for classes decorated with a @
decorator.
- ❌ Incorrect
- ✅ Correct
class Constants {
static readonly version = 42;
}
Open in Playground@logOnRead()
class Constants {
static readonly version = 42;
}
Open in PlaygroundWhen Not To Use It
If your project was set up before modern class and namespace practices, and you don't have the time to switch over, you might not be practically able to use this rule. You might consider using ESLint disable comments for those specific situations instead of completely disabling this rule.