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'[];`, + }, + ], + }, + ], + }), }); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy