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