diff --git a/packages/eslint-plugin/docs/rules/restrict-template-expressions.md b/packages/eslint-plugin/docs/rules/restrict-template-expressions.md index e2d18d8e267..d7afae0ee26 100644 --- a/packages/eslint-plugin/docs/rules/restrict-template-expressions.md +++ b/packages/eslint-plugin/docs/rules/restrict-template-expressions.md @@ -90,6 +90,15 @@ const arg = /foo/; const msg1 = `arg = ${arg}`; ``` +### `allowNever` + +Examples of additional **correct** code for this rule with `{ allowNever: true }`: + +```ts +const arg = 'something'; +const msg1 = typeof arg === 'string' ? arg : `arg = ${arg}`; +``` + ## Related To - [`no-base-to-string`](./no-base-to-string.md) diff --git a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts index 382f8ce0137..485ba42378b 100644 --- a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts +++ b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts @@ -11,6 +11,7 @@ type Options = [ allowAny?: boolean; allowNullish?: boolean; allowRegExp?: boolean; + allowNever?: boolean; }, ]; @@ -58,6 +59,11 @@ export default util.createRule({ 'Whether to allow `regexp` typed values in template expressions.', type: 'boolean', }, + allowNever: { + description: + 'Whether to allow `never` typed values in template expressions.', + type: 'boolean', + }, }, }, ], @@ -111,6 +117,10 @@ export default util.createRule({ return true; } + if (options.allowNever && util.isTypeNeverType(type)) { + return true; + } + return false; } diff --git a/packages/eslint-plugin/tests/rules/restrict-template-expressions.test.ts b/packages/eslint-plugin/tests/rules/restrict-template-expressions.test.ts index 7e80bdbdf5d..b58305051fa 100644 --- a/packages/eslint-plugin/tests/rules/restrict-template-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/restrict-template-expressions.test.ts @@ -227,6 +227,50 @@ ruleTester.run('restrict-template-expressions', rule, { } `, }, + // allowNever + { + options: [{ allowNever: true }], + code: ` + declare const value: never; + const stringy = \`\${value}\`; + `, + }, + { + options: [{ allowNever: true }], + code: ` + const arg = 'hello'; + const msg = typeof arg === 'string' ? arg : \`arg = \${arg}\`; + `, + }, + { + options: [{ allowNever: true }], + code: ` + function test(arg: 'one' | 'two') { + switch (arg) { + case 'one': + return 1; + case 'two': + return 2; + default: + throw new Error(\`Unrecognised arg: \${arg}\`); + } + } + `, + }, + { + options: [{ allowNever: true }], + code: ` + // more variants may be added to Foo in the future + type Foo = { type: 'a'; value: number }; + + function checkFoosAreMatching(foo1: Foo, foo2: Foo) { + if (foo1.type !== foo2.type) { + // since Foo currently only has one variant, this code is never run, and \`foo1.type\` has type \`never\`. + throw new Error(\`expected \${foo1.type}, found \${foo2.type}\`); + } + } + `, + }, // allow ALL { options: [ @@ -235,10 +279,11 @@ ruleTester.run('restrict-template-expressions', rule, { allowBoolean: true, allowNullish: true, allowRegExp: true, + allowNever: true, }, ], code: ` - type All = string | number | boolean | null | undefined | RegExp; + type All = string | number | boolean | null | undefined | RegExp | never; function test(arg: T) { return \`arg = \${arg}\`; } @@ -418,6 +463,21 @@ ruleTester.run('restrict-template-expressions', rule, { }, ], }, + { + options: [{ allowNever: false }], + code: ` + declare const value: never; + const stringy = \`\${value}\`; + `, + errors: [ + { + messageId: 'invalidType', + data: { type: 'never' }, + line: 3, + column: 28, + }, + ], + }, // TS 3.9 change { options: [{ allowAny: true }],