diff --git a/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md index 6bed5b03fe3..72d38f1ed66 100644 --- a/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md +++ b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md @@ -56,6 +56,7 @@ while (typeof str !== 'undefined') { Options may be provided as an object with: +- `allowNullable` to allow `undefined` and `null` in addition to `boolean` as a type of all boolean expressions. (`false` by default). - `ignoreRhs` to skip the check on the right hand side of expressions like `a && b` or `a || b` - allows these operators to be used for their short-circuiting behavior. (`false` by default). ## Related To diff --git a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts index 9c3c5b6ea98..e67e4d1f687 100644 --- a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts +++ b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts @@ -16,6 +16,7 @@ type ExpressionWithTest = type Options = [ { ignoreRhs?: boolean; + allowNullable?: boolean; }, ]; @@ -36,6 +37,9 @@ export default util.createRule({ ignoreRhs: { type: 'boolean', }, + allowNullable: { + type: 'boolean', + }, }, additionalProperties: false, }, @@ -47,9 +51,10 @@ export default util.createRule({ defaultOptions: [ { ignoreRhs: false, + allowNullable: false, }, ], - create(context, [{ ignoreRhs }]) { + create(context, [options]) { const service = util.getParserServices(context); const checker = service.program.getTypeChecker(); @@ -61,7 +66,34 @@ export default util.createRule({ node, ); const type = util.getConstrainedTypeAtLocation(checker, tsNode); - return tsutils.isTypeFlagSet(type, ts.TypeFlags.BooleanLike); + + if (tsutils.isTypeFlagSet(type, ts.TypeFlags.BooleanLike)) { + return true; + } + + // Check variants of union + if (tsutils.isTypeFlagSet(type, ts.TypeFlags.Union)) { + let hasBoolean = false; + for (const ty of (type as ts.UnionType).types) { + if (tsutils.isTypeFlagSet(ty, ts.TypeFlags.BooleanLike)) { + hasBoolean = true; + continue; + } + if (options.allowNullable) { + if (tsutils.isTypeFlagSet(ty, ts.TypeFlags.Null)) { + continue; + } + if (tsutils.isTypeFlagSet(ty, ts.TypeFlags.Undefined)) { + continue; + } + } + // Union variant is something else + return false; + } + return hasBoolean; + } + + return false; } /** @@ -88,7 +120,7 @@ export default util.createRule({ ): void { if ( !isBooleanType(node.left) || - (!ignoreRhs && !isBooleanType(node.right)) + (!options.ignoreRhs && !isBooleanType(node.right)) ) { reportNode(node); } diff --git a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts index 1067b5fa4c6..c95228b6743 100644 --- a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -165,6 +165,15 @@ const boolOrObj = bool || obj; const boolAndObj = bool && obj; `, }, + { + options: [{ allowNullable: true }], + code: ` + const f1 = (x?: boolean) => x ? 1 : 0; + const f2 = (x: boolean | null) => x ? 1 : 0; + const f3 = (x?: true | null) => x ? 1 : 0; + const f4 = (x?: false) => x ? 1 : 0; + `, + }, ], invalid: [ @@ -933,5 +942,24 @@ const objOrBool = obj || bool; const objAndBool = obj && bool; `, }, + { + options: [{ allowNullable: true }], + errors: [ + { + messageId: 'strictBooleanExpression', + line: 2, + column: 44, + }, + { + messageId: 'strictBooleanExpression', + line: 3, + column: 35, + }, + ], + code: ` + const f = (x: null | undefined) => x ? 1 : 0; + const f = (x?: number) => x ? 1 : 0; + `, + }, ], });