diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx
index 538b911049c3..e35aaed0d072 100644
--- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx
+++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx
@@ -23,7 +23,7 @@ This rule reports when a `switch` statement over a value typed as a union of lit
If set to false, this rule will also report when a `switch` statement has a case for everything in a union and _also_ contains a `default` case. Thus, by setting this option to false, the rule becomes stricter.
When a `switch` statement over a union type is exhaustive, a final `default` case would be a form of dead code.
-Additionally, if a new value is added to the union type, a `default` would prevent the `switch-exhaustiveness-check` rule from reporting on the new case not being handled in the `switch` statement.
+Additionally, if a new value is added to the union type and you're using [`considerDefaultExhaustiveForUnions`](#considerDefaultExhaustiveForUnions), a `default` would prevent the `switch-exhaustiveness-check` rule from reporting on the new case not being handled in the `switch` statement.
#### `allowDefaultCaseForExhaustiveSwitch` Caveats
@@ -57,6 +57,57 @@ switch (value) {
Since `value` is a non-union type it requires the switch case to have a default clause only with `requireDefaultForNonUnion` enabled.
+### `considerDefaultExhaustiveForUnions`
+
+{/* insert option description */}
+
+If set to true, a `switch` statement over a union type that includes a `default` case is considered exhaustive.
+Otherwise, the rule enforces explicitly handling every constituent of the union type with their own explicit `case`.
+Keeping this option disabled can be useful if you want to make sure every value added to the union receives explicit handling, with the `default` case reserved for reporting an error.
+
+Examples of code with `{ considerDefaultExhaustiveForUnions: true }`:
+
+
+
+
+```ts option='{ "considerDefaultExhaustiveForUnions": true }' showPlaygroundButton
+declare const literal: 'a' | 'b';
+
+switch (literal) {
+ case 'a':
+ break;
+ default:
+ break;
+}
+```
+
+
+
+
+
+```ts option='{ "considerDefaultExhaustiveForUnions": true }' showPlaygroundButton
+declare const literal: 'a' | 'b';
+
+switch (literal) {
+ case 'a':
+ break;
+ case 'b':
+ break;
+ default:
+ break;
+}
+
+switch (literal) {
+ case 'a':
+ break;
+ case 'b':
+ break;
+}
+```
+
+
+
+
## Examples
When the switch doesn't have exhaustive cases, either filling them all out or adding a default will correct the rule's complaint.
diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts
index c5ebc2970175..6aa6f9f78fdb 100644
--- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts
+++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts
@@ -37,6 +37,13 @@ type Options = [
* @default false
*/
requireDefaultForNonUnion?: boolean;
+
+ /**
+ * If `true`, the `default` clause is used to determine whether the switch statement is exhaustive for union types.
+ *
+ * @default false
+ */
+ considerDefaultExhaustiveForUnions?: boolean;
},
];
@@ -70,6 +77,10 @@ export default createRule({
type: 'boolean',
description: `If 'true', allow 'default' cases on switch statements with exhaustive cases.`,
},
+ considerDefaultExhaustiveForUnions: {
+ type: 'boolean',
+ description: `If 'true', the 'default' clause is used to determine whether the switch statement is exhaustive for union type`,
+ },
requireDefaultForNonUnion: {
type: 'boolean',
description: `If 'true', require a 'default' clause for switches on non-union types.`,
@@ -81,12 +92,19 @@ export default createRule({
defaultOptions: [
{
allowDefaultCaseForExhaustiveSwitch: true,
+ considerDefaultExhaustiveForUnions: false,
requireDefaultForNonUnion: false,
},
],
create(
context,
- [{ allowDefaultCaseForExhaustiveSwitch, requireDefaultForNonUnion }],
+ [
+ {
+ allowDefaultCaseForExhaustiveSwitch,
+ considerDefaultExhaustiveForUnions,
+ requireDefaultForNonUnion,
+ },
+ ],
) {
const services = getParserServices(context);
const checker = services.program.getTypeChecker();
@@ -156,10 +174,13 @@ export default createRule({
const { defaultCase, missingLiteralBranchTypes, symbolName } =
switchMetadata;
- // We only trigger the rule if a `default` case does not exist, since that
- // would disqualify the switch statement from having cases that exactly
- // match the members of a union.
- if (missingLiteralBranchTypes.length > 0 && defaultCase === undefined) {
+ // Unless considerDefaultExhaustiveForUnions is enabled, the presence of a default case
+ // always makes the switch exhaustive.
+ if (!considerDefaultExhaustiveForUnions && defaultCase != null) {
+ return;
+ }
+
+ if (missingLiteralBranchTypes.length > 0) {
context.report({
node: node.discriminant,
messageId: 'switchIsNotExhaustive',
@@ -197,6 +218,8 @@ export default createRule({
): TSESLint.RuleFix {
const lastCase =
node.cases.length > 0 ? node.cases[node.cases.length - 1] : null;
+ const defaultCase = node.cases.find(caseEl => caseEl.test == null);
+
const caseIndent = lastCase
? ' '.repeat(lastCase.loc.start.column)
: // If there are no cases, use indentation of the switch statement and
@@ -244,6 +267,13 @@ export default createRule({
.join('\n');
if (lastCase) {
+ if (defaultCase) {
+ const beforeFixString = missingCases
+ .map(code => `${code}\n${caseIndent}`)
+ .join('');
+
+ return fixer.insertTextBefore(defaultCase, beforeFixString);
+ }
return fixer.insertTextAfter(lastCase, `\n${fixString}`);
}
diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot
index 1b2fd682199f..f763ba7e0ed7 100644
--- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot
+++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot
@@ -17,6 +17,46 @@ switch (value) {
exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 2`] = `
"Incorrect
+Options: { "considerDefaultExhaustiveForUnions": true }
+
+declare const literal: 'a' | 'b';
+
+switch (literal) {
+ ~~~~~~~ Switch is not exhaustive. Cases not matched: "b"
+ case 'a':
+ break;
+ default:
+ break;
+}
+"
+`;
+
+exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 3`] = `
+"Correct
+Options: { "considerDefaultExhaustiveForUnions": true }
+
+declare const literal: 'a' | 'b';
+
+switch (literal) {
+ case 'a':
+ break;
+ case 'b':
+ break;
+ default:
+ break;
+}
+
+switch (literal) {
+ case 'a':
+ break;
+ case 'b':
+ break;
+}
+"
+`;
+
+exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 4`] = `
+"Incorrect
type Day =
| 'Monday'
@@ -39,7 +79,7 @@ switch (day) {
"
`;
-exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 3`] = `
+exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 5`] = `
"Correct
type Day =
@@ -80,7 +120,7 @@ switch (day) {
"
`;
-exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 4`] = `
+exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 6`] = `
"Correct
type Day =
@@ -105,7 +145,7 @@ switch (day) {
"
`;
-exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 5`] = `
+exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 7`] = `
"Incorrect
enum Fruit {
@@ -125,7 +165,7 @@ switch (fruit) {
"
`;
-exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 6`] = `
+exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 8`] = `
"Correct
enum Fruit {
@@ -152,7 +192,7 @@ switch (fruit) {
"
`;
-exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 7`] = `
+exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 9`] = `
"Correct
enum Fruit {
diff --git a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts
index 13869b62c859..3bcbd37a491a 100644
--- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts
+++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts
@@ -810,6 +810,104 @@ switch (value) {
},
],
},
+ {
+ code: `
+declare const literal: 'a' | 'b';
+switch (literal) {
+ case 'a':
+ break;
+ case 'b':
+ break;
+}
+ `,
+ options: [
+ {
+ considerDefaultExhaustiveForUnions: true,
+ },
+ ],
+ },
+ {
+ code: `
+declare const literal: 'a' | 'b';
+switch (literal) {
+ case 'a':
+ break;
+ case 'b':
+ break;
+ default:
+ break;
+}
+ `,
+ options: [
+ {
+ considerDefaultExhaustiveForUnions: true,
+ },
+ ],
+ },
+ {
+ code: `
+declare const literal: 'a' | 'b';
+switch (literal) {
+ case 'a':
+ break;
+ case 'b':
+ break;
+}
+ `,
+ options: [
+ {
+ allowDefaultCaseForExhaustiveSwitch: false,
+ considerDefaultExhaustiveForUnions: true,
+ },
+ ],
+ },
+ {
+ code: `
+enum MyEnum {
+ Foo = 'Foo',
+ Bar = 'Bar',
+ Baz = 'Baz',
+}
+
+declare const myEnum: MyEnum;
+
+switch (myEnum) {
+ case MyEnum.Foo:
+ break;
+ case MyEnum.Bar:
+ break;
+ case MyEnum.Baz:
+ break;
+ default: {
+ break;
+ }
+}
+ `,
+ options: [
+ {
+ considerDefaultExhaustiveForUnions: true,
+ },
+ ],
+ },
+ {
+ code: `
+declare const value: boolean;
+switch (value) {
+ case false:
+ break;
+ case true:
+ break;
+ default: {
+ break;
+ }
+}
+ `,
+ options: [
+ {
+ considerDefaultExhaustiveForUnions: true,
+ },
+ ],
+ },
],
invalid: [
{
@@ -2373,5 +2471,252 @@ switch (myValue) {
},
],
},
+ {
+ code: `
+declare const literal: 'a' | 'b';
+
+switch (literal) {
+ case 'a':
+ break;
+ default:
+ break;
+}
+ `,
+ errors: [
+ {
+ column: 9,
+ line: 4,
+ messageId: 'switchIsNotExhaustive',
+ suggestions: [
+ {
+ messageId: 'addMissingCases',
+ output: `
+declare const literal: 'a' | 'b';
+
+switch (literal) {
+ case 'a':
+ break;
+ case "b": { throw new Error('Not implemented yet: "b" case') }
+ default:
+ break;
+}
+ `,
+ },
+ ],
+ },
+ ],
+ options: [
+ {
+ considerDefaultExhaustiveForUnions: true,
+ },
+ ],
+ },
+ {
+ code: `
+declare const literal: 'a' | 'b';
+
+switch (literal) {
+ case 'a':
+ break;
+}
+ `,
+ errors: [
+ {
+ column: 9,
+ line: 4,
+ messageId: 'switchIsNotExhaustive',
+ suggestions: [
+ {
+ messageId: 'addMissingCases',
+ output: `
+declare const literal: 'a' | 'b';
+
+switch (literal) {
+ case 'a':
+ break;
+ case "b": { throw new Error('Not implemented yet: "b" case') }
+}
+ `,
+ },
+ ],
+ },
+ ],
+ options: [
+ {
+ considerDefaultExhaustiveForUnions: true,
+ },
+ ],
+ },
+ {
+ code: `
+declare const literal: 'a' | 'b';
+
+switch (literal) {
+ default:
+ case 'a':
+ break;
+}
+ `,
+ errors: [
+ {
+ column: 9,
+ line: 4,
+ messageId: 'switchIsNotExhaustive',
+ suggestions: [
+ {
+ messageId: 'addMissingCases',
+ output: `
+declare const literal: 'a' | 'b';
+
+switch (literal) {
+ case "b": { throw new Error('Not implemented yet: "b" case') }
+ default:
+ case 'a':
+ break;
+}
+ `,
+ },
+ ],
+ },
+ ],
+ options: [
+ {
+ considerDefaultExhaustiveForUnions: true,
+ },
+ ],
+ },
+ {
+ code: `
+declare const literal: 'a' | 'b' | 'c';
+
+switch (literal) {
+ case 'a':
+ break;
+ default:
+ break;
+}
+ `,
+ errors: [
+ {
+ column: 9,
+ line: 4,
+ messageId: 'switchIsNotExhaustive',
+ suggestions: [
+ {
+ messageId: 'addMissingCases',
+ output: `
+declare const literal: 'a' | 'b' | 'c';
+
+switch (literal) {
+ case 'a':
+ break;
+ case "b": { throw new Error('Not implemented yet: "b" case') }
+ case "c": { throw new Error('Not implemented yet: "c" case') }
+ default:
+ break;
+}
+ `,
+ },
+ ],
+ },
+ ],
+ options: [
+ {
+ considerDefaultExhaustiveForUnions: true,
+ },
+ ],
+ },
+ {
+ code: `
+enum MyEnum {
+ Foo = 'Foo',
+ Bar = 'Bar',
+ Baz = 'Baz',
+}
+
+declare const myEnum: MyEnum;
+
+switch (myEnum) {
+ case MyEnum.Foo:
+ break;
+ default: {
+ break;
+ }
+}
+ `,
+ errors: [
+ {
+ column: 9,
+ line: 10,
+ messageId: 'switchIsNotExhaustive',
+ suggestions: [
+ {
+ messageId: 'addMissingCases',
+ output: `
+enum MyEnum {
+ Foo = 'Foo',
+ Bar = 'Bar',
+ Baz = 'Baz',
+}
+
+declare const myEnum: MyEnum;
+
+switch (myEnum) {
+ case MyEnum.Foo:
+ break;
+ case MyEnum.Bar: { throw new Error('Not implemented yet: MyEnum.Bar case') }
+ case MyEnum.Baz: { throw new Error('Not implemented yet: MyEnum.Baz case') }
+ default: {
+ break;
+ }
+}
+ `,
+ },
+ ],
+ },
+ ],
+ options: [
+ {
+ considerDefaultExhaustiveForUnions: true,
+ },
+ ],
+ },
+ {
+ code: `
+declare const value: boolean;
+switch (value) {
+ default: {
+ break;
+ }
+}
+ `,
+ errors: [
+ {
+ column: 9,
+ line: 3,
+ messageId: 'switchIsNotExhaustive',
+ suggestions: [
+ {
+ messageId: 'addMissingCases',
+ output: `
+declare const value: boolean;
+switch (value) {
+ case false: { throw new Error('Not implemented yet: false case') }
+ case true: { throw new Error('Not implemented yet: true case') }
+ default: {
+ break;
+ }
+}
+ `,
+ },
+ ],
+ },
+ ],
+ options: [
+ {
+ considerDefaultExhaustiveForUnions: true,
+ },
+ ],
+ },
],
});
diff --git a/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot b/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot
index 6146dadc128b..05cbeedd6162 100644
--- a/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot
+++ b/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot
@@ -12,6 +12,10 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos
"description": "If 'true', allow 'default' cases on switch statements with exhaustive cases.",
"type": "boolean"
},
+ "considerDefaultExhaustiveForUnions": {
+ "description": "If 'true', the 'default' clause is used to determine whether the switch statement is exhaustive for union type",
+ "type": "boolean"
+ },
"requireDefaultForNonUnion": {
"description": "If 'true', require a 'default' clause for switches on non-union types.",
"type": "boolean"
@@ -28,6 +32,8 @@ type Options = [
{
/** If 'true', allow 'default' cases on switch statements with exhaustive cases. */
allowDefaultCaseForExhaustiveSwitch?: boolean;
+ /** If 'true', the 'default' clause is used to determine whether the switch statement is exhaustive for union type */
+ considerDefaultExhaustiveForUnions?: boolean;
/** If 'true', require a 'default' clause for switches on non-union types. */
requireDefaultForNonUnion?: boolean;
},