diff --git a/packages/eslint-plugin/docs/rules/consistent-type-assertions.md b/packages/eslint-plugin/docs/rules/consistent-type-assertions.md index 4c7f87ee9558..19f96878c7f6 100644 --- a/packages/eslint-plugin/docs/rules/consistent-type-assertions.md +++ b/packages/eslint-plugin/docs/rules/consistent-type-assertions.md @@ -103,6 +103,34 @@ const foo = ; +### `arrayLiteralTypeAssertions` + +Always prefer `const x: T[] = [ ... ];` to `const x = [ ... ] as T[];` (or similar with angle brackets). The rationale for this is exactly the same as for `objectLiteralTypeAssertions`. + +The const assertion `const x = [1, 2, 3] as const`, introduced in TypeScript 3.4, is considered beneficial and is ignored by this option. + +Assertions to `any` are also ignored by this option. + +Examples of code for `{ assertionStyle: 'as', arrayLiteralTypeAssertions: 'never' }`: + + + +#### ❌ Incorrect + +```ts +const x = [] as string[]; +const y = ['a'] as string[]; +``` + +#### ✅ Correct + +```ts +const x: string[] = []; +const y: string[] = ['a']; +``` + + + ## When Not To Use It If you do not want to enforce consistent type assertions. diff --git a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts index 58403fe8a141..77dbf039e565 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts @@ -9,12 +9,16 @@ type MessageIds = | 'angle-bracket' | 'never' | 'unexpectedObjectTypeAssertion' + | 'unexpectedArrayTypeAssertion' | 'replaceObjectTypeAssertionWithAnnotation' - | 'replaceObjectTypeAssertionWithSatisfies'; + | 'replaceObjectTypeAssertionWithSatisfies' + | 'replaceArrayTypeAssertionWithAnnotation' + | 'replaceArrayTypeAssertionWithSatisfies'; type OptUnion = | { assertionStyle: 'as' | 'angle-bracket'; objectLiteralTypeAssertions?: 'allow' | 'allow-as-parameter' | 'never'; + arrayLiteralTypeAssertions?: 'allow' | 'never'; } | { assertionStyle: 'never'; @@ -36,10 +40,15 @@ export default util.createRule({ 'angle-bracket': "Use '<{{cast}}>' instead of 'as {{cast}}'.", never: 'Do not use any type assertions.', unexpectedObjectTypeAssertion: 'Always prefer const x: T = { ... }.', + unexpectedArrayTypeAssertion: 'Always prefer const x: T[] = [ ... ].', replaceObjectTypeAssertionWithAnnotation: 'Use const x: {{cast}} = { ... } instead.', replaceObjectTypeAssertionWithSatisfies: 'Use const x = { ... } satisfies {{cast}} instead.', + replaceArrayTypeAssertionWithAnnotation: + 'Use const x: [{cast}] = [ ... ] instead.', + replaceArrayTypeAssertionWithSatisfies: + 'Use const x = [ ... ] satisfies [{cast}] instead.', }, schema: [ { @@ -63,6 +72,9 @@ export default util.createRule({ objectLiteralTypeAssertions: { enum: ['allow', 'allow-as-parameter', 'never'], }, + arrayLiteralTypeAssertions: { + enum: ['allow', 'never'], + }, }, additionalProperties: false, required: ['assertionStyle'], @@ -75,6 +87,7 @@ export default util.createRule({ { assertionStyle: 'as', objectLiteralTypeAssertions: 'allow', + arrayLiteralTypeAssertions: 'allow', }, ], create(context, [options]) { @@ -164,7 +177,46 @@ export default util.createRule({ } } - function checkExpression( + function getReplacementSuggestions( + node: TSESTree.TSTypeAssertion | TSESTree.TSAsExpression, + annotationMessageId: MessageIds, + satisfiesMessageId: MessageIds, + ): TSESLint.ReportSuggestionArray { + const suggest: TSESLint.ReportSuggestionArray = []; + if ( + node.parent?.type === AST_NODE_TYPES.VariableDeclarator && + !node.parent.id.typeAnnotation + ) { + const { parent } = node; + suggest.push({ + messageId: annotationMessageId, + data: { cast: sourceCode.getText(node.typeAnnotation) }, + fix: fixer => [ + fixer.insertTextAfter( + parent.id, + `: ${sourceCode.getText(node.typeAnnotation)}`, + ), + fixer.replaceText(node, getTextWithParentheses(node.expression)), + ], + }); + } + suggest.push({ + messageId: satisfiesMessageId, + data: { cast: sourceCode.getText(node.typeAnnotation) }, + fix: fixer => [ + fixer.replaceText(node, getTextWithParentheses(node.expression)), + fixer.insertTextAfter( + node, + ` satisfies ${context + .getSourceCode() + .getText(node.typeAnnotation)}`, + ), + ], + }); + return suggest; + } + + function checkExpressionForObjectAssertion( node: TSESTree.TSTypeAssertion | TSESTree.TSAsExpression, ): void { if ( @@ -191,37 +243,11 @@ export default util.createRule({ checkType(node.typeAnnotation) && node.expression.type === AST_NODE_TYPES.ObjectExpression ) { - const suggest: TSESLint.ReportSuggestionArray = []; - if ( - node.parent?.type === AST_NODE_TYPES.VariableDeclarator && - !node.parent.id.typeAnnotation - ) { - const { parent } = node; - suggest.push({ - messageId: 'replaceObjectTypeAssertionWithAnnotation', - data: { cast: sourceCode.getText(node.typeAnnotation) }, - fix: fixer => [ - fixer.insertTextAfter( - parent.id, - `: ${sourceCode.getText(node.typeAnnotation)}`, - ), - fixer.replaceText(node, getTextWithParentheses(node.expression)), - ], - }); - } - suggest.push({ - messageId: 'replaceObjectTypeAssertionWithSatisfies', - data: { cast: sourceCode.getText(node.typeAnnotation) }, - fix: fixer => [ - fixer.replaceText(node, getTextWithParentheses(node.expression)), - fixer.insertTextAfter( - node, - ` satisfies ${context - .getSourceCode() - .getText(node.typeAnnotation)}`, - ), - ], - }); + const suggest = getReplacementSuggestions( + node, + 'replaceObjectTypeAssertionWithAnnotation', + 'replaceObjectTypeAssertionWithSatisfies', + ); context.report({ node, @@ -231,6 +257,35 @@ export default util.createRule({ } } + function checkExpressionForArrayAssertion( + node: TSESTree.TSTypeAssertion | TSESTree.TSAsExpression, + ): void { + if ( + options.assertionStyle === 'never' || + options.arrayLiteralTypeAssertions === 'allow' || + node.expression.type !== AST_NODE_TYPES.ArrayExpression + ) { + return; + } + + if ( + checkType(node.typeAnnotation) && + node.expression.type === AST_NODE_TYPES.ArrayExpression + ) { + const suggest = getReplacementSuggestions( + node, + 'replaceArrayTypeAssertionWithAnnotation', + 'replaceArrayTypeAssertionWithSatisfies', + ); + + context.report({ + node, + messageId: 'unexpectedArrayTypeAssertion', + suggest, + }); + } + } + return { TSTypeAssertion(node): void { if (options.assertionStyle !== 'angle-bracket') { @@ -238,7 +293,8 @@ export default util.createRule({ return; } - checkExpression(node); + checkExpressionForObjectAssertion(node); + checkExpressionForArrayAssertion(node); }, TSAsExpression(node): void { if (options.assertionStyle !== 'as') { @@ -246,7 +302,8 @@ export default util.createRule({ return; } - checkExpression(node); + checkExpressionForObjectAssertion(node); + checkExpressionForArrayAssertion(node); }, }; }, diff --git a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts index 45f672af9e4d..07733e7a67b2 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts @@ -62,6 +62,21 @@ print?.({ bar: 5 }) print?.call({ bar: 5 }) `; +const ARRAY_LITERAL_AS_CASTS = ` +const x = [] as string[]; +const x = ['a'] as string[]; +const x = [] as Array; +const x = ['a'] as Array; +const x = [Math.random() ? 'a' : 'b'] as 'a'[]; +`; +const ARRAY_LITERAL_ANGLE_BRACKET_CASTS = ` +const x = []; +const x = ['a']; +const x = >[]; +const x = >['a']; +const x = <'a'[]>[Math.random() ? 'a' : 'b']; +`; + ruleTester.run('consistent-type-assertions', rule, { valid: [ ...batchedSingleLineTests({ @@ -118,6 +133,22 @@ ruleTester.run('consistent-type-assertions', rule, { }, ], }), + ...batchedSingleLineTests({ + code: ARRAY_LITERAL_AS_CASTS, + options: [ + { + assertionStyle: 'as', + }, + ], + }), + ...batchedSingleLineTests({ + code: ARRAY_LITERAL_ANGLE_BRACKET_CASTS, + options: [ + { + assertionStyle: 'angle-bracket', + }, + ], + }), { code: 'const x = [1];', options: [ @@ -710,5 +741,309 @@ ruleTester.run('consistent-type-assertions', rule, { }, ], }, + ...batchedSingleLineTests({ + code: ARRAY_LITERAL_AS_CASTS, + options: [ + { + assertionStyle: 'never', + }, + ], + errors: [ + { + messageId: 'never', + line: 2, + }, + { + messageId: 'never', + line: 3, + }, + { + messageId: 'never', + line: 4, + }, + { + messageId: 'never', + line: 5, + }, + { + messageId: 'never', + line: 6, + }, + ], + }), + ...batchedSingleLineTests({ + code: ARRAY_LITERAL_ANGLE_BRACKET_CASTS, + options: [ + { + assertionStyle: 'never', + }, + ], + errors: [ + { + messageId: 'never', + line: 2, + }, + { + messageId: 'never', + line: 3, + }, + { + messageId: 'never', + line: 4, + }, + { + messageId: 'never', + line: 5, + }, + { + messageId: 'never', + line: 6, + }, + ], + }), + ...batchedSingleLineTests({ + code: ARRAY_LITERAL_AS_CASTS, + options: [ + { + assertionStyle: 'angle-bracket', + }, + ], + errors: [ + { + messageId: 'angle-bracket', + line: 2, + }, + { + messageId: 'angle-bracket', + line: 3, + }, + { + messageId: 'angle-bracket', + line: 4, + }, + { + messageId: 'angle-bracket', + line: 5, + }, + { + messageId: 'angle-bracket', + line: 6, + }, + ], + output: null, + }), + ...batchedSingleLineTests({ + code: ARRAY_LITERAL_ANGLE_BRACKET_CASTS, + options: [ + { + assertionStyle: 'as', + }, + ], + errors: [ + { + messageId: 'as', + line: 2, + }, + { + messageId: 'as', + line: 3, + }, + { + messageId: 'as', + line: 4, + }, + { + messageId: 'as', + line: 5, + }, + { + messageId: 'as', + line: 6, + }, + ], + output: ARRAY_LITERAL_AS_CASTS, + }), + ...batchedSingleLineTests({ + code: ARRAY_LITERAL_AS_CASTS, + options: [ + { + assertionStyle: 'as', + arrayLiteralTypeAssertions: 'never', + }, + ], + errors: [ + { + messageId: 'unexpectedArrayTypeAssertion', + line: 2, + suggestions: [ + { + messageId: 'replaceArrayTypeAssertionWithAnnotation', + data: { cast: 'string' }, + output: 'const x: string[] = [];', + }, + { + messageId: 'replaceArrayTypeAssertionWithSatisfies', + data: { cast: 'string' }, + output: 'const x = [] satisfies string[];', + }, + ], + }, + { + messageId: 'unexpectedArrayTypeAssertion', + line: 3, + suggestions: [ + { + messageId: 'replaceArrayTypeAssertionWithAnnotation', + data: { cast: 'string' }, + output: `const x: string[] = ['a'];`, + }, + { + messageId: 'replaceArrayTypeAssertionWithSatisfies', + data: { cast: 'string' }, + output: `const x = ['a'] satisfies string[];`, + }, + ], + }, + { + messageId: 'unexpectedArrayTypeAssertion', + line: 4, + suggestions: [ + { + messageId: 'replaceArrayTypeAssertionWithAnnotation', + data: { cast: 'string' }, + output: 'const x: Array = [];', + }, + { + messageId: 'replaceArrayTypeAssertionWithSatisfies', + data: { cast: 'string' }, + output: 'const x = [] satisfies Array;', + }, + ], + }, + { + messageId: 'unexpectedArrayTypeAssertion', + line: 5, + suggestions: [ + { + messageId: 'replaceArrayTypeAssertionWithAnnotation', + data: { cast: 'string' }, + output: `const x: Array = ['a'];`, + }, + { + messageId: 'replaceArrayTypeAssertionWithSatisfies', + data: { cast: 'string' }, + output: `const x = ['a'] satisfies Array;`, + }, + ], + }, + { + messageId: 'unexpectedArrayTypeAssertion', + line: 6, + suggestions: [ + { + messageId: 'replaceArrayTypeAssertionWithAnnotation', + data: { cast: 'string' }, + output: `const x: 'a'[] = [Math.random() ? 'a' : 'b'];`, + }, + { + messageId: 'replaceArrayTypeAssertionWithSatisfies', + data: { cast: 'string' }, + output: `const x = [Math.random() ? 'a' : 'b'] satisfies 'a'[];`, + }, + ], + }, + ], + }), ], + ...batchedSingleLineTests({ + code: ARRAY_LITERAL_ANGLE_BRACKET_CASTS, + options: [ + { + assertionStyle: 'angle-brackets', + arrayLiteralTypeAssertions: 'never', + }, + ], + errors: [ + { + messageId: 'unexpectedArrayTypeAssertion', + line: 2, + suggestions: [ + { + messageId: 'replaceArrayTypeAssertionWithAnnotation', + data: { cast: 'string' }, + output: 'const x: string[] = [];', + }, + { + messageId: 'replaceArrayTypeAssertionWithSatisfies', + data: { cast: 'string' }, + output: 'const x = [] satisfies string[];', + }, + ], + }, + { + messageId: 'unexpectedArrayTypeAssertion', + line: 3, + suggestions: [ + { + messageId: 'replaceArrayTypeAssertionWithAnnotation', + data: { cast: 'string' }, + output: `const x: string[] = ['a'];`, + }, + { + messageId: 'replaceArrayTypeAssertionWithSatisfies', + data: { cast: 'string' }, + output: `const x = ['a'] satisfies string[];`, + }, + ], + }, + { + messageId: 'unexpectedArrayTypeAssertion', + line: 4, + suggestions: [ + { + messageId: 'replaceArrayTypeAssertionWithAnnotation', + data: { cast: 'string' }, + output: 'const x: Array = [];', + }, + { + messageId: 'replaceArrayTypeAssertionWithSatisfies', + data: { cast: 'string' }, + output: 'const x = [] satisfies Array;', + }, + ], + }, + { + messageId: 'unexpectedArrayTypeAssertion', + line: 5, + suggestions: [ + { + messageId: 'replaceArrayTypeAssertionWithAnnotation', + data: { cast: 'string' }, + output: `const x: Array = ['a'];`, + }, + { + messageId: 'replaceArrayTypeAssertionWithSatisfies', + data: { cast: 'string' }, + output: `const x = ['a'] satisfies Array;`, + }, + ], + }, + { + messageId: 'unexpectedArrayTypeAssertion', + line: 6, + suggestions: [ + { + messageId: 'replaceArrayTypeAssertionWithAnnotation', + data: { cast: 'string' }, + output: `const x: 'a'[] = [Math.random() ? 'a' : 'b'];`, + }, + { + messageId: 'replaceArrayTypeAssertionWithSatisfies', + data: { cast: 'string' }, + output: `const x = [Math.random() ? 'a' : 'b'] satisfies 'a'[];`, + }, + ], + }, + ], + }), });