diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index 983eb1472c14..34da070722ad 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -1,6 +1,7 @@ import type { ScopeManager, ScopeVariable, + Variable, } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; @@ -188,7 +189,7 @@ class UnusedVarsVisitor extends Visitor { // basic exported variables isExported(variable) || // variables implicitly exported via a merged declaration - isMergableExported(variable) || + isMergeableExported(variable) || // used variables isUsedVariable(variable) ) { @@ -415,6 +416,14 @@ function isSelfReference( return false; } +/** + * @param variable the variable to check + * @returns true if it is `isTypeVariable` and `isValueVariable` + */ +function isDualPurposeVariable(variable: Variable): boolean { + return variable.isTypeVariable && variable.isValueVariable; +} + const MERGABLE_TYPES = new Set([ AST_NODE_TYPES.ClassDeclaration, AST_NODE_TYPES.FunctionDeclaration, @@ -426,7 +435,7 @@ const MERGABLE_TYPES = new Set([ * Determine if the variable is directly exported * @param variable the variable to check */ -function isMergableExported(variable: ScopeVariable): boolean { +function isMergeableExported(variable: Variable): boolean { // If all of the merged things are of the same type, TS will error if not all of them are exported - so we only need to find one for (const def of variable.defs) { // parameters can never be exported. @@ -441,7 +450,10 @@ function isMergableExported(variable: ScopeVariable): boolean { def.node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration) || def.node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration ) { - return true; + return !( + def.node.type === AST_NODE_TYPES.TSTypeAliasDeclaration && + isDualPurposeVariable(variable) + ); } } @@ -453,7 +465,7 @@ function isMergableExported(variable: ScopeVariable): boolean { * @param variable eslint-scope variable object. * @returns True if the variable is exported, false if not. */ -function isExported(variable: ScopeVariable): boolean { +function isExported(variable: Variable): boolean { return variable.defs.some(definition => { let node = definition.node; @@ -465,7 +477,15 @@ function isExported(variable: ScopeVariable): boolean { } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return node.parent!.type.startsWith('Export'); + const isExportedFlag = node.parent!.type.startsWith('Export'); + + return ( + isExportedFlag && + !( + node.type === AST_NODE_TYPES.TSTypeAliasDeclaration && + isDualPurposeVariable(variable) + ) + ); }); } diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts index 5f40bbcdc3cc..517baaefc6aa 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts @@ -1715,6 +1715,44 @@ export {}; ], filename: 'foo.d.ts', }, + // https://github.com/typescript-eslint/typescript-eslint/issues/10658 + { + code: ` +const A = 0; +export type A = typeof A; + `, + errors: [ + { + data: { + action: 'assigned a value', + additional: '', + varName: 'A', + }, + line: 2, + messageId: 'usedOnlyAsType', + }, + ], + }, + { + code: ` +function A() {} +namespace A { + export const prop = 1; +} +export type A = typeof A; + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'A', + }, + line: 2, + messageId: 'usedOnlyAsType', + }, + ], + }, ], valid: [ @@ -3018,5 +3056,18 @@ declare class Bar {} `, filename: 'foo.d.ts', }, + { + code: ` +const A = 0; +type A = typeof A; +export { A }; + `, + }, + { + code: ` +class A {} +export type B = A; + `, + }, ], });