🌐 AI搜索 & 代理 主页
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
description: 'Disallow unused private class members.'
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

> 🛑 This file is source code, not the primary documentation location! 🛑
>
> See **https://typescript-eslint.io/rules/no-unused-private-class-members** for documentation.

This rule extends the base [`eslint/no-unused-private-class-members`](https://eslint.org/docs/rules/no-unused-private-class-members) rule.
It adds support for members declared with TypeScript's `private` keyword.

## Options

This rule has no options.

## Examples

<Tabs>
<TabItem value="❌ Incorrect">

```ts
class A {
private foo = 123;
}
```

</TabItem>
<TabItem value="✅ Correct">

```tsx
class A {
private foo = 123;

constructor() {
console.log(this.foo);
}
}
```

</TabItem>
</Tabs>

## Limitations

This rule does not detect the following cases:

(1) Private members only used via a variable that would require type analysis to resolve the type.

```ts
type T = Foo;
class Foo {
private prop = 123;

method1(a: Foo) {
// ✅ Detected as a usage
const prop = this.prop;

// ✅ Detected as a usage (variables with explicit and simple type annotations are handled)
const otherProp = a.prop;
}

method2(a: T) {
// ❌ NOT detected as a usage (complex type annotation that requires type information to handle)
const prop = a.prop;
}
}
```

(2) Usages of the private member outside of the class:

```ts
class Foo {
private prop = 123;
}

const instance = new Foo();
// ❌ NOT detected as a usage
console.log(foo['prop']);
```

(3) Reassignments of `this` multiple times:

```ts
class Foo {
private prop = 123;

foo() {
const self1 = this;
const self2 = self1;
return self2.prop;
}
}
```

(4) Mutable reassignments of `this`:

```ts
class Foo {
private prop = 123;
private parent: Foo | null;

foo() {
let self = this;
while (self.parent != null) {
self = self.parent;
}
return self.prop;
}
}
```

## When Not To Use It

If you don't want to be notified about unused private class members, you can safely turn this rule off.
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/configs/eslintrc/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export = {
'@typescript-eslint/no-unsafe-unary-minus': 'error',
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': 'error',
'no-unused-private-class-members': 'off',
'@typescript-eslint/no-unused-private-class-members': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'no-use-before-define': 'off',
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/configs/flat/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export default (
'@typescript-eslint/no-unsafe-unary-minus': 'error',
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': 'error',
'no-unused-private-class-members': 'off',
'@typescript-eslint/no-unused-private-class-members': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'no-use-before-define': 'off',
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import noUnsafeReturn from './no-unsafe-return';
import noUnsafeTypeAssertion from './no-unsafe-type-assertion';
import noUnsafeUnaryMinus from './no-unsafe-unary-minus';
import noUnusedExpressions from './no-unused-expressions';
import noUnusedPrivateClassMembers from './no-unused-private-class-members';
import noUnusedVars from './no-unused-vars';
import noUseBeforeDefine from './no-use-before-define';
import noUselessConstructor from './no-useless-constructor';
Expand Down Expand Up @@ -222,6 +223,7 @@ const rules = {
'no-unsafe-type-assertion': noUnsafeTypeAssertion,
'no-unsafe-unary-minus': noUnsafeUnaryMinus,
'no-unused-expressions': noUnusedExpressions,
'no-unused-private-class-members': noUnusedPrivateClassMembers,
'no-unused-vars': noUnusedVars,
'no-use-before-define': noUseBeforeDefine,
'no-useless-constructor': noUselessConstructor,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ESLintUtils } from '@typescript-eslint/utils';

import { createRule } from '../util';
import { analyzeClassMemberUsage } from '../util/class-scope-analyzer/classScopeAnalyzer';

type Options = [];
export type MessageIds = 'unusedPrivateClassMember';

export default createRule<Options, MessageIds>({
name: 'no-unused-private-class-members',
meta: {
type: 'problem',
docs: {
description: 'Disallow unused private class members',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a bug and shouldn't be reported (deploy preview playground link):

class Test1 {
  // reported but shouldn't?
  private foo: number | null;

  public bar() {
    this.foo ??= 1;
  }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a weird case that isn't currently reported by no-unused-vars but really should be.

It is reported by the base no-unused-private-class-members rule

Copy link
Member

@kirkwaiblinger kirkwaiblinger Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was just a recent eslint bug report about these; I guess it's intentional for no-unused-vars since x ?? = y reads x first (equivalent to x != null ? x : (x = y)) rather than unconditionally assigning (x = x != null ? x : y). See eslint/eslint#20029. But presumably the two rules should agree?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, but do note that these are reported by no-useless-assignment so 🤷‍♂️ 🤷‍♂️

(playground)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably file a bug for NUPCM then so it matches NUV?

extendsBaseRule: true,
requiresTypeChecking: false,
},

messages: {
unusedPrivateClassMember:
"Private class member '{{classMemberName}}' is defined but never used.",
},

schema: [],
},
defaultOptions: [],
create(context) {
return {
'Program:exit'(node) {
const result = analyzeClassMemberUsage(
node,
ESLintUtils.nullThrows(
context.sourceCode.scopeManager,
'Missing required scope manager',
),
);

for (const classScope of result.values()) {
for (const member of [
...classScope.members.instance.values(),
...classScope.members.static.values(),
]) {
if (
(!member.isPrivate() && !member.isHashPrivate()) ||
member.isUsed()
) {
continue;
}

context.report({
node: member.nameNode,
messageId: 'unusedPrivateClassMember',
data: { classMemberName: member.name },
});
}
}
},
};
},
});
Loading