diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index 29e956d314a2..fe759d35303d 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -8009,6 +8009,20 @@ const testsTypescript = { } `, }, + { + // Regression test for facebook/react#27335: typeof with nested + // property access (TSQualifiedName) must not require the root + // identifier as a dependency. + code: normalizeIndent` + function MyComponent() { + const [foo, setFoo] = React.useState<{bar: number}>({bar: 42}); + React.useEffect(() => { + const square = (x: typeof foo.bar) => x * x; + setFoo(previous => ({...previous, bar: square(previous.bar)})); + }, []); + } + `, + }, { code: normalizeIndent` function MyComponent() { diff --git a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts index 6b790680608d..dd9b475c5bd5 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts @@ -548,9 +548,17 @@ const rule = { }); } + // Skip references that appear inside a TypeScript type position. + // Walk up through TSQualifiedName chains so that nested accesses + // like `typeof foo.bar.baz` or `Foo.Bar.Baz` are also skipped, not + // just the direct `typeof foo` / `Foo` cases (fixes #27335). + let typeAncestor = dependencyNode.parent; + while (typeAncestor?.type === 'TSQualifiedName') { + typeAncestor = typeAncestor.parent; + } if ( - dependencyNode.parent?.type === 'TSTypeQuery' || - dependencyNode.parent?.type === 'TSTypeReference' + typeAncestor?.type === 'TSTypeQuery' || + typeAncestor?.type === 'TSTypeReference' ) { continue; }