diff --git a/.eslintrc.js b/.eslintrc.js index 4aa35ae4305..3271a33e83e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -33,6 +33,7 @@ module.exports = { '@typescript-eslint/prefer-nullish-coalescing': 'error', '@typescript-eslint/prefer-optional-chain': 'error', '@typescript-eslint/unbound-method': 'off', + '@typescript-eslint/prefer-as-const': 'error', 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': [ diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 77709f77832..b91ab9182f4 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -133,6 +133,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | :thought_balloon: | | [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | | +| [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | | :wrench: | | | [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Prefer a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated | | | | | [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures | | :wrench: | | | [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | :heavy_check_mark: | :wrench: | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/prefer-as-const.md b/packages/eslint-plugin/docs/rules/prefer-as-const.md new file mode 100644 index 00000000000..bb288192d26 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-as-const.md @@ -0,0 +1,28 @@ +# Prefer usage of `as const` over literal type (`prefer-as-const`) + +This rule recommends usage of `const` assertion when type primitive value is equal to type. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```ts +let bar: 2 = 2; +let foo = <'bar'>'bar'; +let foo = { bar: 'baz' as 'baz' }; +``` + +Examples of **correct** code for this rule: + +```ts +let foo = 'bar'; +let foo = 'bar' as const; +let foo: 'bar' = 'bar' as const; +let bar = 'bar' as string; +let foo = 'bar'; +let foo = { bar: 'baz' }; +``` + +## When Not To Use It + +If you are using typescript < 3.4 diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 1ee81fe91ca..916d9a0238f 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -65,6 +65,7 @@ "no-useless-constructor": "off", "@typescript-eslint/no-useless-constructor": "error", "@typescript-eslint/no-var-requires": "error", + "@typescript-eslint/prefer-as-const": "error", "@typescript-eslint/prefer-for-of": "error", "@typescript-eslint/prefer-function-type": "error", "@typescript-eslint/prefer-includes": "error", diff --git a/packages/eslint-plugin/src/rules/indent.ts b/packages/eslint-plugin/src/rules/indent.ts index 46cca6acd15..0a8d3196ab6 100644 --- a/packages/eslint-plugin/src/rules/indent.ts +++ b/packages/eslint-plugin/src/rules/indent.ts @@ -260,7 +260,7 @@ export default util.createRule({ return rules.VariableDeclaration({ type: AST_NODE_TYPES.VariableDeclaration, - kind: 'const' as 'const', + kind: 'const' as const, declarations: [ { type: AST_NODE_TYPES.VariableDeclarator, @@ -389,7 +389,7 @@ export default util.createRule({ ? node.typeAnnotation.loc.end : squareBracketStart.loc.end, }, - kind: 'init' as 'init', + kind: 'init' as const, computed: false, method: false, shorthand: false, diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 7f9f47e538a..2079ccc4e40 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -56,6 +56,7 @@ import noUnusedVarsExperimental from './no-unused-vars-experimental'; import noUseBeforeDefine from './no-use-before-define'; import noUselessConstructor from './no-useless-constructor'; import noVarRequires from './no-var-requires'; +import preferAsConst from './prefer-as-const'; import preferForOf from './prefer-for-of'; import preferFunctionType from './prefer-function-type'; import preferIncludes from './prefer-includes'; @@ -140,6 +141,7 @@ export default { 'no-use-before-define': noUseBeforeDefine, 'no-useless-constructor': noUselessConstructor, 'no-var-requires': noVarRequires, + 'prefer-as-const': preferAsConst, 'prefer-for-of': preferForOf, 'prefer-function-type': preferFunctionType, 'prefer-includes': preferIncludes, diff --git a/packages/eslint-plugin/src/rules/prefer-as-const.ts b/packages/eslint-plugin/src/rules/prefer-as-const.ts new file mode 100644 index 00000000000..1cf21af7580 --- /dev/null +++ b/packages/eslint-plugin/src/rules/prefer-as-const.ts @@ -0,0 +1,78 @@ +import { + AST_NODE_TYPES, + TSESLint, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; + +export default util.createRule({ + name: 'prefer-as-const', + meta: { + type: 'suggestion', + docs: { + description: 'Prefer usage of `as const` over literal type', + category: 'Best Practices', + recommended: false, + }, + fixable: 'code', + messages: { + preferConstAssertion: + 'Expected a `const` instead of a literal type assertion', + variableConstAssertion: + 'Expected a `const` assertion instead of a literal type annotation', + variableSuggest: 'You should use `as const` instead of type annotation.', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + function compareTypes( + valueNode: TSESTree.Expression, + typeNode: TSESTree.TypeNode, + canFix: boolean, + ): void { + if ( + valueNode.type === AST_NODE_TYPES.Literal && + typeNode.type === AST_NODE_TYPES.TSLiteralType && + 'raw' in typeNode.literal && + valueNode.raw === typeNode.literal.raw + ) { + if (canFix) { + context.report({ + node: typeNode, + messageId: 'preferConstAssertion', + fix: fixer => fixer.replaceText(typeNode, 'const'), + }); + } else { + context.report({ + node: typeNode, + messageId: 'variableConstAssertion', + suggest: [ + { + messageId: 'variableSuggest', + fix: (fixer): TSESLint.RuleFix[] => [ + fixer.remove(typeNode.parent!), + fixer.insertTextAfter(valueNode, ' as const'), + ], + }, + ], + }); + } + } + } + + return { + TSAsExpression(node): void { + compareTypes(node.expression, node.typeAnnotation, true); + }, + TSTypeAssertion(node): void { + compareTypes(node.expression, node.typeAnnotation, true); + }, + VariableDeclarator(node): void { + if (node.init && node.id.typeAnnotation) { + compareTypes(node.init, node.id.typeAnnotation.typeAnnotation, false); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-array-constructor.test.ts b/packages/eslint-plugin/tests/rules/no-array-constructor.test.ts index ca3da767f80..cc6530085dd 100644 --- a/packages/eslint-plugin/tests/rules/no-array-constructor.test.ts +++ b/packages/eslint-plugin/tests/rules/no-array-constructor.test.ts @@ -6,7 +6,7 @@ const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', }); -const messageId = 'useLiteral' as 'useLiteral'; +const messageId = 'useLiteral' as const; ruleTester.run('no-array-constructor', rule, { valid: [ diff --git a/packages/eslint-plugin/tests/rules/no-extraneous-class.test.ts b/packages/eslint-plugin/tests/rules/no-extraneous-class.test.ts index e009aa153ae..88d558e711a 100644 --- a/packages/eslint-plugin/tests/rules/no-extraneous-class.test.ts +++ b/packages/eslint-plugin/tests/rules/no-extraneous-class.test.ts @@ -3,13 +3,13 @@ import rule from '../../src/rules/no-extraneous-class'; import { RuleTester } from '../RuleTester'; const empty = { - messageId: 'empty' as 'empty', + messageId: 'empty' as const, }; const onlyStatic = { - messageId: 'onlyStatic' as 'onlyStatic', + messageId: 'onlyStatic' as const, }; const onlyConstructor = { - messageId: 'onlyConstructor' as 'onlyConstructor', + messageId: 'onlyConstructor' as const, }; const ruleTester = new RuleTester({ diff --git a/packages/eslint-plugin/tests/rules/no-this-alias.test.ts b/packages/eslint-plugin/tests/rules/no-this-alias.test.ts index 03723eb9515..0cbd35dae19 100644 --- a/packages/eslint-plugin/tests/rules/no-this-alias.test.ts +++ b/packages/eslint-plugin/tests/rules/no-this-alias.test.ts @@ -3,15 +3,15 @@ import rule from '../../src/rules/no-this-alias'; import { RuleTester } from '../RuleTester'; const idError = { - messageId: 'thisAssignment' as 'thisAssignment', + messageId: 'thisAssignment' as const, type: AST_NODE_TYPES.Identifier, }; const destructureError = { - messageId: 'thisDestructure' as 'thisDestructure', + messageId: 'thisDestructure' as const, type: AST_NODE_TYPES.ObjectPattern, }; const arrayDestructureError = { - messageId: 'thisDestructure' as 'thisDestructure', + messageId: 'thisDestructure' as const, type: AST_NODE_TYPES.ArrayPattern, }; diff --git a/packages/eslint-plugin/tests/rules/no-use-before-define.test.ts b/packages/eslint-plugin/tests/rules/no-use-before-define.test.ts index 8b375aef075..9d9434397fd 100644 --- a/packages/eslint-plugin/tests/rules/no-use-before-define.test.ts +++ b/packages/eslint-plugin/tests/rules/no-use-before-define.test.ts @@ -6,7 +6,7 @@ const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', }); -const parserOptions = { ecmaVersion: 6 as 6 }; +const parserOptions = { ecmaVersion: 6 as const }; ruleTester.run('no-use-before-define', rule, { valid: [ diff --git a/packages/eslint-plugin/tests/rules/prefer-as-const.test.ts b/packages/eslint-plugin/tests/rules/prefer-as-const.test.ts new file mode 100644 index 00000000000..c18ac5d7f14 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/prefer-as-const.test.ts @@ -0,0 +1,156 @@ +import rule from '../../src/rules/prefer-as-const'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('prefer-as-const', rule, { + valid: [ + "let foo = 'baz' as const", + 'let foo = 1 as const', + "let foo = { bar: 'baz' as const }", + 'let foo = { bar: 1 as const }', + "let foo = { bar: 'baz' }", + 'let foo = { bar: 2 }', + "let foo = 'bar';", + "let foo = 'bar';", + "let foo = 'bar' as string;", + 'let foo = `bar` as `bar`;', + 'let foo = `bar` as `foo`;', + "let foo = `bar` as 'bar';", + "let foo: string = 'bar';", + 'let foo: number = 1;', + "let foo: 'bar' = baz;", + "let foo = 'bar';", + 'class foo { bar: "baz" = "baz" }', + 'class foo { bar = "baz" }', + "let foo: 'bar'", + 'let foo = { bar }', + "let foo: 'baz' = 'baz' as const", + ], + invalid: [ + { + code: "let foo = { bar: 'baz' as 'baz' }", + output: "let foo = { bar: 'baz' as const }", + errors: [ + { + messageId: 'preferConstAssertion', + line: 1, + column: 27, + }, + ], + }, + { + code: 'let foo = { bar: 1 as 1 }', + output: 'let foo = { bar: 1 as const }', + errors: [ + { + messageId: 'preferConstAssertion', + line: 1, + column: 23, + }, + ], + }, + { + code: "let []: 'bar' = 'bar';", + output: "let []: 'bar' = 'bar';", + errors: [ + { + messageId: 'variableConstAssertion', + line: 1, + column: 9, + }, + ], + }, + { + code: "let foo: 'bar' = 'bar';", + output: "let foo: 'bar' = 'bar';", + errors: [ + { + messageId: 'variableConstAssertion', + line: 1, + column: 10, + suggestions: [ + { + messageId: 'variableSuggest', + output: "let foo = 'bar' as const;", + }, + ], + }, + ], + }, + { + code: 'let foo: 2 = 2;', + output: 'let foo: 2 = 2;', + errors: [ + { + messageId: 'variableConstAssertion', + line: 1, + column: 10, + suggestions: [ + { + messageId: 'variableSuggest', + output: 'let foo = 2 as const;', + }, + ], + }, + ], + }, + { + code: "let foo: 'bar' = 'bar' as 'bar';", + output: "let foo: 'bar' = 'bar' as const;", + errors: [ + { + messageId: 'preferConstAssertion', + line: 1, + column: 27, + }, + ], + }, + { + code: "let foo = <'bar'>'bar';", + output: "let foo = 'bar';", + errors: [ + { + messageId: 'preferConstAssertion', + line: 1, + column: 12, + }, + ], + }, + { + code: 'let foo = <4>4;', + output: 'let foo = 4;', + errors: [ + { + messageId: 'preferConstAssertion', + line: 1, + column: 12, + }, + ], + }, + { + code: "let foo = 'bar' as 'bar';", + output: "let foo = 'bar' as const;", + errors: [ + { + messageId: 'preferConstAssertion', + line: 1, + column: 20, + }, + ], + }, + { + code: 'let foo = 5 as 5;', + output: 'let foo = 5 as const;', + errors: [ + { + messageId: 'preferConstAssertion', + line: 1, + column: 16, + }, + ], + }, + ], +}); diff --git a/packages/parser/tests/lib/parser.ts b/packages/parser/tests/lib/parser.ts index 56ce08b6786..3881456d2ff 100644 --- a/packages/parser/tests/lib/parser.ts +++ b/packages/parser/tests/lib/parser.ts @@ -43,7 +43,7 @@ describe('parser', () => { comment: false, range: false, tokens: false, - sourceType: 'module' as 'module', + sourceType: 'module' as const, ecmaVersion: 2018, ecmaFeatures: { globalReturn: false, diff --git a/packages/typescript-estree/src/node-utils.ts b/packages/typescript-estree/src/node-utils.ts index 77ffca87cc4..ffcd0af0b57 100644 --- a/packages/typescript-estree/src/node-utils.ts +++ b/packages/typescript-estree/src/node-utils.ts @@ -30,70 +30,70 @@ const LOGICAL_OPERATORS: ( ]; const TOKEN_TO_TEXT = { - [SyntaxKind.OpenBraceToken]: '{' as '{', - [SyntaxKind.CloseBraceToken]: '}' as '}', - [SyntaxKind.OpenParenToken]: '(' as '(', - [SyntaxKind.CloseParenToken]: ')' as ')', - [SyntaxKind.OpenBracketToken]: '[' as '[', - [SyntaxKind.CloseBracketToken]: ']' as ']', - [SyntaxKind.DotToken]: '.' as '.', - [SyntaxKind.DotDotDotToken]: '...' as '...', - [SyntaxKind.SemicolonToken]: ';' as ';', - [SyntaxKind.CommaToken]: ',' as ',', - [SyntaxKind.LessThanToken]: '<' as '<', - [SyntaxKind.GreaterThanToken]: '>' as '>', - [SyntaxKind.LessThanEqualsToken]: '<=' as '<=', - [SyntaxKind.GreaterThanEqualsToken]: '>=' as '>=', - [SyntaxKind.EqualsEqualsToken]: '==' as '==', - [SyntaxKind.ExclamationEqualsToken]: '!=' as '!=', - [SyntaxKind.EqualsEqualsEqualsToken]: '===' as '===', - [SyntaxKind.InstanceOfKeyword]: 'instanceof' as 'instanceof', - [SyntaxKind.ExclamationEqualsEqualsToken]: '!==' as '!==', - [SyntaxKind.EqualsGreaterThanToken]: '=>' as '=>', - [SyntaxKind.PlusToken]: '+' as '+', - [SyntaxKind.MinusToken]: '-' as '-', - [SyntaxKind.AsteriskToken]: '*' as '*', - [SyntaxKind.AsteriskAsteriskToken]: '**' as '**', - [SyntaxKind.SlashToken]: '/' as '/', - [SyntaxKind.PercentToken]: '%' as '%', - [SyntaxKind.PlusPlusToken]: '++' as '++', - [SyntaxKind.MinusMinusToken]: '--' as '--', - [SyntaxKind.LessThanLessThanToken]: '<<' as '<<', - [SyntaxKind.LessThanSlashToken]: '>' as '>>', - [SyntaxKind.GreaterThanGreaterThanGreaterThanToken]: '>>>' as '>>>', - [SyntaxKind.AmpersandToken]: '&' as '&', - [SyntaxKind.BarToken]: '|' as '|', - [SyntaxKind.CaretToken]: '^' as '^', - [SyntaxKind.ExclamationToken]: '!' as '!', - [SyntaxKind.TildeToken]: '~' as '~', - [SyntaxKind.AmpersandAmpersandToken]: '&&' as '&&', - [SyntaxKind.BarBarToken]: '||' as '||', - [SyntaxKind.QuestionToken]: '?' as '?', - [SyntaxKind.ColonToken]: ':' as ':', - [SyntaxKind.EqualsToken]: '=' as '=', - [SyntaxKind.PlusEqualsToken]: '+=' as '+=', - [SyntaxKind.MinusEqualsToken]: '-=' as '-=', - [SyntaxKind.AsteriskEqualsToken]: '*=' as '*=', - [SyntaxKind.AsteriskAsteriskEqualsToken]: '**=' as '**=', - [SyntaxKind.SlashEqualsToken]: '/=' as '/=', - [SyntaxKind.PercentEqualsToken]: '%=' as '%=', - [SyntaxKind.LessThanLessThanEqualsToken]: '<<=' as '<<=', - [SyntaxKind.GreaterThanGreaterThanEqualsToken]: '>>=' as '>>=', - [SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: '>>>=' as '>>>=', - [SyntaxKind.AmpersandEqualsToken]: '&=' as '&=', - [SyntaxKind.BarEqualsToken]: '|=' as '|=', - [SyntaxKind.CaretEqualsToken]: '^=' as '^=', - [SyntaxKind.AtToken]: '@' as '@', - [SyntaxKind.InKeyword]: 'in' as 'in', - [SyntaxKind.UniqueKeyword]: 'unique' as 'unique', - [SyntaxKind.KeyOfKeyword]: 'keyof' as 'keyof', - [SyntaxKind.NewKeyword]: 'new' as 'new', - [SyntaxKind.ImportKeyword]: 'import' as 'import', - [SyntaxKind.ReadonlyKeyword]: 'readonly' as 'readonly', - [SyntaxKind.QuestionQuestionToken]: '??' as '??', - [SyntaxKind.QuestionDotToken]: '?.' as '?.', -}; + [SyntaxKind.OpenBraceToken]: '{', + [SyntaxKind.CloseBraceToken]: '}', + [SyntaxKind.OpenParenToken]: '(', + [SyntaxKind.CloseParenToken]: ')', + [SyntaxKind.OpenBracketToken]: '[', + [SyntaxKind.CloseBracketToken]: ']', + [SyntaxKind.DotToken]: '.', + [SyntaxKind.DotDotDotToken]: '...', + [SyntaxKind.SemicolonToken]: ';', + [SyntaxKind.CommaToken]: ',', + [SyntaxKind.LessThanToken]: '<', + [SyntaxKind.GreaterThanToken]: '>', + [SyntaxKind.LessThanEqualsToken]: '<=', + [SyntaxKind.GreaterThanEqualsToken]: '>=', + [SyntaxKind.EqualsEqualsToken]: '==', + [SyntaxKind.ExclamationEqualsToken]: '!=', + [SyntaxKind.EqualsEqualsEqualsToken]: '===', + [SyntaxKind.InstanceOfKeyword]: 'instanceof', + [SyntaxKind.ExclamationEqualsEqualsToken]: '!==', + [SyntaxKind.EqualsGreaterThanToken]: '=>', + [SyntaxKind.PlusToken]: '+', + [SyntaxKind.MinusToken]: '-', + [SyntaxKind.AsteriskToken]: '*', + [SyntaxKind.AsteriskAsteriskToken]: '**', + [SyntaxKind.SlashToken]: '/', + [SyntaxKind.PercentToken]: '%', + [SyntaxKind.PlusPlusToken]: '++', + [SyntaxKind.MinusMinusToken]: '--', + [SyntaxKind.LessThanLessThanToken]: '<<', + [SyntaxKind.LessThanSlashToken]: '>', + [SyntaxKind.GreaterThanGreaterThanGreaterThanToken]: '>>>', + [SyntaxKind.AmpersandToken]: '&', + [SyntaxKind.BarToken]: '|', + [SyntaxKind.CaretToken]: '^', + [SyntaxKind.ExclamationToken]: '!', + [SyntaxKind.TildeToken]: '~', + [SyntaxKind.AmpersandAmpersandToken]: '&&', + [SyntaxKind.BarBarToken]: '||', + [SyntaxKind.QuestionToken]: '?', + [SyntaxKind.ColonToken]: ':', + [SyntaxKind.EqualsToken]: '=', + [SyntaxKind.PlusEqualsToken]: '+=', + [SyntaxKind.MinusEqualsToken]: '-=', + [SyntaxKind.AsteriskEqualsToken]: '*=', + [SyntaxKind.AsteriskAsteriskEqualsToken]: '**=', + [SyntaxKind.SlashEqualsToken]: '/=', + [SyntaxKind.PercentEqualsToken]: '%=', + [SyntaxKind.LessThanLessThanEqualsToken]: '<<=', + [SyntaxKind.GreaterThanGreaterThanEqualsToken]: '>>=', + [SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: '>>>=', + [SyntaxKind.AmpersandEqualsToken]: '&=', + [SyntaxKind.BarEqualsToken]: '|=', + [SyntaxKind.CaretEqualsToken]: '^=', + [SyntaxKind.AtToken]: '@', + [SyntaxKind.InKeyword]: 'in', + [SyntaxKind.UniqueKeyword]: 'unique', + [SyntaxKind.KeyOfKeyword]: 'keyof', + [SyntaxKind.NewKeyword]: 'new', + [SyntaxKind.ImportKeyword]: 'import', + [SyntaxKind.ReadonlyKeyword]: 'readonly', + [SyntaxKind.QuestionQuestionToken]: '??', + [SyntaxKind.QuestionDotToken]: '?.', +} as const; /** * Returns true if the given ts.Token is the assignment operator