diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index b5819ddb1f0f..34ebfc018d6a 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -197,6 +197,28 @@ export default createRule({ ); } + function isNullableMemberExpression( + node: TSESTree.MemberExpression, + ): boolean { + const objectType = services.getTypeAtLocation(node.object); + if (node.computed) { + const propertyType = services.getTypeAtLocation(node.property); + return isNullablePropertyType(objectType, propertyType); + } + const property = node.property; + + if (property.type === AST_NODE_TYPES.Identifier) { + const propertyType = objectType.getProperty(property.name); + if ( + propertyType && + tsutils.isSymbolFlagSet(propertyType, ts.SymbolFlags.Optional) + ) { + return true; + } + } + return false; + } + /** * Checks if a conditional node is necessary: * if the type of the node is always true or always false, it's not necessary. @@ -283,7 +305,13 @@ export default createRule({ let messageId: MessageId | null = null; if (isTypeFlagSet(type, ts.TypeFlags.Never)) { messageId = 'never'; - } else if (!isPossiblyNullish(type)) { + } else if ( + !isPossiblyNullish(type) && + !( + node.type === AST_NODE_TYPES.MemberExpression && + isNullableMemberExpression(node) + ) + ) { // Since typescript array index signature types don't represent the // possibility of out-of-bounds access, if we're indexing into an array // just skip the check, to avoid false positives diff --git a/packages/eslint-plugin/tests/fixtures/tsconfig.exactOptionalPropertyTypes.json b/packages/eslint-plugin/tests/fixtures/tsconfig.exactOptionalPropertyTypes.json new file mode 100644 index 000000000000..67060ba22c57 --- /dev/null +++ b/packages/eslint-plugin/tests/fixtures/tsconfig.exactOptionalPropertyTypes.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "exactOptionalPropertyTypes": true + } +} diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index a144c7026c4b..b4f6356efef4 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -22,6 +22,11 @@ const ruleTester = new RuleTester({ }, }); +const optionsWithExactOptionalPropertyTypes = { + tsconfigRootDir: rootPath, + project: './tsconfig.exactOptionalPropertyTypes.json', +}; + const ruleError = ( line: number, column: number, @@ -748,6 +753,48 @@ declare let foo: number; foo ||= 1; `, ` +declare const foo: { bar: { baz?: number; qux: number } }; +type Key = 'baz' | 'qux'; +declare const key: Key; +foo.bar[key] ??= 1; + `, + ` +enum Keys { + A = 'A', + B = 'B', +} +type Foo = { + [Keys.A]: number | null; + [Keys.B]: number; +}; +declare const foo: Foo; +declare const key: Keys; +foo[key] ??= 1; + `, + { + code: ` +declare const foo: { bar?: number }; +foo.bar ??= 1; + `, + parserOptions: optionsWithExactOptionalPropertyTypes, + }, + { + code: ` +declare const foo: { bar: { baz?: number } }; +foo['bar'].baz ??= 1; + `, + parserOptions: optionsWithExactOptionalPropertyTypes, + }, + { + code: ` +declare const foo: { bar: { baz?: number; qux: number } }; +type Key = 'baz' | 'qux'; +declare const key: Key; +foo.bar[key] ??= 1; + `, + parserOptions: optionsWithExactOptionalPropertyTypes, + }, + ` declare let foo: number; foo &&= 1; `, @@ -2035,6 +2082,22 @@ foo &&= null; }, ], }, + { + code: ` +declare const foo: { bar: number }; +foo.bar ??= 1; + `, + parserOptions: optionsWithExactOptionalPropertyTypes, + errors: [ + { + messageId: 'neverNullish', + line: 3, + endLine: 3, + column: 1, + endColumn: 8, + }, + ], + }, { code: noFormat` type Foo = { bar: () => number } | null;