diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 007a8a07a6d6..c5de895264ed 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -28,13 +28,23 @@ const isTruthyLiteral = (type: ts.Type): boolean => const isPossiblyFalsy = (type: ts.Type): boolean => tsutils .unionTypeParts(type) + // Intersections like `string & {}` can also be possibly falsy, + // requiring us to look into the intersection. + .flatMap(type => tsutils.intersectionTypeParts(type)) // PossiblyFalsy flag includes literal values, so exclude ones that // are definitely truthy .filter(t => !isTruthyLiteral(t)) .some(type => isTypeFlagSet(type, ts.TypeFlags.PossiblyFalsy)); const isPossiblyTruthy = (type: ts.Type): boolean => - tsutils.unionTypeParts(type).some(type => !tsutils.isFalsyType(type)); + tsutils + .unionTypeParts(type) + .map(type => tsutils.intersectionTypeParts(type)) + .some(intersectionParts => + // It is possible to define intersections that are always falsy, + // like `"" & { __brand: string }`. + intersectionParts.every(type => !tsutils.isFalsyType(type)), + ); // Nullish utilities const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null; 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 c68c735ae11e..b72eb4f13c19 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -83,6 +83,34 @@ const result2 = foo() == null; necessaryConditionTest('null | object'), necessaryConditionTest('undefined | true'), necessaryConditionTest('void | true'), + // "branded" type + necessaryConditionTest('string & {}'), + necessaryConditionTest('string & { __brand: string }'), + necessaryConditionTest('number & { __brand: string }'), + necessaryConditionTest('boolean & { __brand: string }'), + necessaryConditionTest('bigint & { __brand: string }'), + necessaryConditionTest('string & {} & { __brand: string }'), + necessaryConditionTest( + 'string & { __brandA: string } & { __brandB: string }', + ), + necessaryConditionTest('string & { __brand: string } | number'), + necessaryConditionTest('(string | number) & { __brand: string }'), + necessaryConditionTest('string & ({ __brand: string } | number)'), + necessaryConditionTest('("" | "foo") & { __brand: string }'), + necessaryConditionTest( + '(string & { __brandA: string }) | (number & { __brandB: string })', + ), + necessaryConditionTest( + '((string & { __brandA: string }) | (number & { __brandB: string }) & ("" | "foo"))', + ), + necessaryConditionTest( + '{ __brandA: string} & (({ __brandB: string } & string) | ({ __brandC: string } & number))', + ), + necessaryConditionTest( + '(string | number) & ("foo" | 123 | { __brandA: string })', + ), + + necessaryConditionTest('string & string'), necessaryConditionTest('any'), // any necessaryConditionTest('unknown'), // unknown @@ -645,6 +673,7 @@ const t1 = b2 && b1 ? 'yes' : 'no'; unnecessaryConditionTest('null', 'alwaysFalsy'), unnecessaryConditionTest('void', 'alwaysFalsy'), unnecessaryConditionTest('never', 'never'), + unnecessaryConditionTest('string & number', 'never'), // More complex logical expressions { @@ -1821,5 +1850,37 @@ foo &&= null; }, ], }, + + // "branded" types + unnecessaryConditionTest('"" & {}', 'alwaysFalsy'), + unnecessaryConditionTest('"" & { __brand: string }', 'alwaysFalsy'), + unnecessaryConditionTest( + '("" | false) & { __brand: string }', + 'alwaysFalsy', + ), + unnecessaryConditionTest( + '((string & { __brandA: string }) | (number & { __brandB: string })) & ""', + 'alwaysFalsy', + ), + unnecessaryConditionTest( + '("foo" | "bar") & { __brand: string }', + 'alwaysTruthy', + ), + unnecessaryConditionTest( + '(123 | true) & { __brand: string }', + 'alwaysTruthy', + ), + unnecessaryConditionTest( + '(string | number) & ("foo" | 123) & { __brand: string }', + 'alwaysTruthy', + ), + unnecessaryConditionTest( + '((string & { __brandA: string }) | (number & { __brandB: string })) & "foo"', + 'alwaysTruthy', + ), + unnecessaryConditionTest( + '((string & { __brandA: string }) | (number & { __brandB: string })) & ("foo" | 123)', + 'alwaysTruthy', + ), ], });