Summary
eslint-plugin-tsdoc's published lib/index.d.ts imports from 'eslint', but the package declares no dependency or peerDependency on eslint, only a devDependency. Under non-hoisted layouts (pnpm, Yarn PnP), that import never reaches the consumer's ESLint; it binds to whatever @types/eslint is reachable in the tree. When that's the v8-era @types/eslint, // @ts-checked flat configs fail to compile.
Repro
https://github.com/michaeldev5/bug-reproduction/tree/eslint-plugin-tsdoc-undeclared-eslint-types
git clone -b eslint-plugin-tsdoc-undeclared-eslint-types \
https://github.com/michaeldev5/bug-reproduction.git
cd bug-reproduction && pnpm install
./node_modules/.bin/tsc --noEmit
A // @ts-checked flat config doing plugins: { tsdoc } fails:
eslint.config.js(6,14): error TS2322: Type 'IPlugin' is not assignable to type 'Plugin'.
Types of property 'rules' are incompatible.
Type '{ [x: string]: RuleModule; }' is not assignable to
type 'Record<string, RuleDefinition<RuleDefinitionTypeOptions>>'.
eslint-plugin-tsdoc@0.5.2, eslint@10.8.0, typescript@6.0.3, pnpm 11.
Cause
tsc --traceResolution shows the plugin's own import type * as eslint from 'eslint' landing on the wrong package:
======== Resolving module 'eslint' from '.../eslint-plugin-tsdoc/lib/index.d.ts'. ========
======== Module name 'eslint' was successfully resolved to
'.../.pnpm/@types+eslint@9.6.1/node_modules/@types/eslint/index.d.ts' ========
Because eslint is undeclared, pnpm never links it into the plugin's private node_modules, so TypeScript's upward walk continues past it. In the repro, @types/eslint arrives purely transitively via @types/eslint-scope; in real projects it usually comes through webpack. Nothing depends on it directly.
The declared shape isn't itself wrong. Against ESLint's own bundled typings, { [k: string]: Rule.RuleModule } is assignable to ESLint.Plugin["rules"]. The bug is only which 'eslint' the declaration file binds to, which is also why a flat npm install doesn't reproduce: the real eslint is hoisted and its bundled types win.
Fix
"peerDependencies": { "eslint": ">=7" }
Verified sufficient: declaring the peer via pnpm packageExtensions links the consumer's ESLint into the plugin's node_modules, resolution moves to the real package, and tsc exits 0.
More durably, the published typings could drop the ambient 'eslint' import and use @typescript-eslint/utils (already a runtime dependency) or @eslint/core's Plugin / RuleDefinition. That removes the undeclared-module dependency entirely, and composes with #425.
Summary
eslint-plugin-tsdoc's publishedlib/index.d.tsimports from'eslint', but the package declares nodependencyorpeerDependencyoneslint, only adevDependency. Under non-hoisted layouts (pnpm, Yarn PnP), that import never reaches the consumer's ESLint; it binds to whatever@types/eslintis reachable in the tree. When that's the v8-era@types/eslint,// @ts-checked flat configs fail to compile.Repro
https://github.com/michaeldev5/bug-reproduction/tree/eslint-plugin-tsdoc-undeclared-eslint-types
A
// @ts-checked flat config doingplugins: { tsdoc }fails:eslint-plugin-tsdoc@0.5.2,eslint@10.8.0,typescript@6.0.3, pnpm 11.Cause
tsc --traceResolutionshows the plugin's ownimport type * as eslint from 'eslint'landing on the wrong package:Because
eslintis undeclared, pnpm never links it into the plugin's privatenode_modules, so TypeScript's upward walk continues past it. In the repro,@types/eslintarrives purely transitively via@types/eslint-scope; in real projects it usually comes throughwebpack. Nothing depends on it directly.The declared shape isn't itself wrong. Against ESLint's own bundled typings,
{ [k: string]: Rule.RuleModule }is assignable toESLint.Plugin["rules"]. The bug is only which'eslint'the declaration file binds to, which is also why a flatnpm installdoesn't reproduce: the realeslintis hoisted and its bundled types win.Fix
Verified sufficient: declaring the peer via pnpm
packageExtensionslinks the consumer's ESLint into the plugin'snode_modules, resolution moves to the real package, andtscexits 0.More durably, the published typings could drop the ambient
'eslint'import and use@typescript-eslint/utils(already a runtime dependency) or@eslint/core'sPlugin/RuleDefinition. That removes the undeclared-module dependency entirely, and composes with #425.