Skip to content

feat(eslint-plugin): [consistent-type-assertions] add arrayLiteralTypeAssertions options #10565

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 13, 2025
73 changes: 73 additions & 0 deletions packages/eslint-plugin/docs/rules/consistent-type-assertions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,79 @@ const foo = <Foo props={{ bar: 1 } as Bar} />;
</TabItem>
</Tabs>

### `arrayLiteralTypeAssertions`

{/* insert option description */}

Always prefer `const x: T[] = [ ... ];` to `const x = [ ... ] as T[];` (or similar with angle brackets).

The compiler will warn for excess properties of elements with this syntax, but not missing _required_ fields of those objects.
For example: `const x: {foo: number}[] = [{}];` will fail to compile, but `const x = [{}] as [{ foo: number }]` will succeed.

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' }`:

<Tabs>
<TabItem value="❌ Incorrect">

```ts option='{ "assertionStyle": "as", "arrayLiteralTypeAssertions": "never" }'
const x = ['foo'] as T;

function bar() {
return ['foo'] as T;
}
```

</TabItem>
<TabItem value="✅ Correct">

```ts option='{ "assertionStyle": "as", "arrayLiteralTypeAssertions": "never" }'
const x: T = ['foo'];
const y = ['foo'] as any;
const z = ['foo'] as unknown;

function bar(): T {
return ['foo'];
}
```

</TabItem>
</Tabs>

Examples of code for `{ assertionStyle: 'as', arrayLiteralTypeAssertions: 'allow-as-parameter' }`:

<Tabs>
<TabItem value="❌ Incorrect">

```ts option='{ "assertionStyle": "as", "arrayLiteralTypeAssertions": "allow-as-parameter" }'
const x = ['foo'] as T;

function bar() {
return ['foo'] as T;
}
```

</TabItem>
<TabItem value="✅ Correct">

```tsx option='{ "assertionStyle": "as", "arrayLiteralTypeAssertions": "allow-as-parameter" }'
const x: T = ['foo'];
const y = ['foo'] as any;
const z = ['foo'] as unknown;
bar(['foo'] as T);
new Clazz(['foo'] as T);
function bar() {
throw ['foo'] as Foo;
}
const foo = <Foo props={['foo'] as Bar} />;
```

</TabItem>
</Tabs>

## When Not To Use It

If you do not want to enforce consistent type assertions.
Expand Down
169 changes: 121 additions & 48 deletions packages/eslint-plugin/src/rules/consistent-type-assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,27 @@ export type MessageIds =
| 'angle-bracket'
| 'as'
| 'never'
| 'replaceArrayTypeAssertionWithAnnotation'
| 'replaceArrayTypeAssertionWithSatisfies'
| 'replaceObjectTypeAssertionWithAnnotation'
| 'replaceObjectTypeAssertionWithSatisfies'
| 'unexpectedArrayTypeAssertion'
| 'unexpectedObjectTypeAssertion';
type OptUnion =
| {
assertionStyle: 'angle-bracket' | 'as';
objectLiteralTypeAssertions?: 'allow' | 'allow-as-parameter' | 'never';
arrayLiteralTypeAssertions?: 'allow' | 'allow-as-parameter' | 'never';
}
| {
assertionStyle: 'never';
};
export type Options = readonly [OptUnion];

type AsExpressionOrTypeAssertion =
| TSESTree.TSAsExpression
| TSESTree.TSTypeAssertion;

export default createRule<Options, MessageIds>({
name: 'consistent-type-assertions',
meta: {
Expand All @@ -45,10 +53,15 @@ export default createRule<Options, MessageIds>({
'angle-bracket': "Use '<{{cast}}>' instead of 'as {{cast}}'.",
as: "Use 'as {{cast}}' instead of '<{{cast}}>'.",
never: 'Do not use any type assertions.',
replaceArrayTypeAssertionWithAnnotation:
'Use const x: {{cast}} = [ ... ] instead.',
replaceArrayTypeAssertionWithSatisfies:
'Use const x = [ ... ] satisfies {{cast}} instead.',
replaceObjectTypeAssertionWithAnnotation:
'Use const x: {{cast}} = { ... } instead.',
replaceObjectTypeAssertionWithSatisfies:
'Use const x = { ... } satisfies {{cast}} instead.',
unexpectedArrayTypeAssertion: 'Always prefer const x: T[] = [ ... ].',
unexpectedObjectTypeAssertion: 'Always prefer const x: T = { ... }.',
},
schema: [
Expand All @@ -70,6 +83,12 @@ export default createRule<Options, MessageIds>({
type: 'object',
additionalProperties: false,
properties: {
arrayLiteralTypeAssertions: {
type: 'string',
description:
'Whether to always prefer type declarations for array literals used as variable initializers, rather than type assertions.',
enum: ['allow', 'allow-as-parameter', 'never'],
},
assertionStyle: {
type: 'string',
description: 'The expected assertion style to enforce.',
Expand All @@ -89,6 +108,7 @@ export default createRule<Options, MessageIds>({
},
defaultOptions: [
{
arrayLiteralTypeAssertions: 'allow',
assertionStyle: 'as',
objectLiteralTypeAssertions: 'allow',
},
Expand All @@ -106,7 +126,7 @@ export default createRule<Options, MessageIds>({
}

function reportIncorrectAssertionType(
node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion,
node: AsExpressionOrTypeAssertion,
): void {
const messageId = options.assertionStyle;

Expand Down Expand Up @@ -192,8 +212,63 @@ export default createRule<Options, MessageIds>({
}
}

function checkExpression(
node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion,
function getSuggestions(
node: AsExpressionOrTypeAssertion,
annotationMessageId: MessageIds,
satisfiesMessageId: MessageIds,
): TSESLint.ReportSuggestionArray<MessageIds> {
const suggestions: TSESLint.ReportSuggestionArray<MessageIds> = [];
if (
node.parent.type === AST_NODE_TYPES.VariableDeclarator &&
!node.parent.id.typeAnnotation
) {
const { parent } = node;
suggestions.push({
messageId: annotationMessageId,
data: { cast: context.sourceCode.getText(node.typeAnnotation) },
fix: fixer => [
fixer.insertTextAfter(
parent.id,
`: ${context.sourceCode.getText(node.typeAnnotation)}`,
),
fixer.replaceText(
node,
getTextWithParentheses(context.sourceCode, node.expression),
),
],
});
}
suggestions.push({
messageId: satisfiesMessageId,
data: { cast: context.sourceCode.getText(node.typeAnnotation) },
fix: fixer => [
fixer.replaceText(
node,
getTextWithParentheses(context.sourceCode, node.expression),
),
fixer.insertTextAfter(
node,
` satisfies ${context.sourceCode.getText(node.typeAnnotation)}`,
),
],
});
return suggestions;
}

function isAsParameter(node: AsExpressionOrTypeAssertion): boolean {
return (
node.parent.type === AST_NODE_TYPES.NewExpression ||
node.parent.type === AST_NODE_TYPES.CallExpression ||
node.parent.type === AST_NODE_TYPES.ThrowStatement ||
node.parent.type === AST_NODE_TYPES.AssignmentPattern ||
node.parent.type === AST_NODE_TYPES.JSXExpressionContainer ||
(node.parent.type === AST_NODE_TYPES.TemplateLiteral &&
node.parent.parent.type === AST_NODE_TYPES.TaggedTemplateExpression)
Comment on lines +260 to +266
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Testing]: Some of these can be removed with no tests failing, I think it would be helpful to have these covered by tests.

);
}

function checkExpressionForObjectAssertion(
node: AsExpressionOrTypeAssertion,
): void {
if (
options.assertionStyle === 'never' ||
Expand All @@ -205,54 +280,17 @@ export default createRule<Options, MessageIds>({

if (
options.objectLiteralTypeAssertions === 'allow-as-parameter' &&
(node.parent.type === AST_NODE_TYPES.NewExpression ||
node.parent.type === AST_NODE_TYPES.CallExpression ||
node.parent.type === AST_NODE_TYPES.ThrowStatement ||
node.parent.type === AST_NODE_TYPES.AssignmentPattern ||
node.parent.type === AST_NODE_TYPES.JSXExpressionContainer ||
(node.parent.type === AST_NODE_TYPES.TemplateLiteral &&
node.parent.parent.type ===
AST_NODE_TYPES.TaggedTemplateExpression))
isAsParameter(node)
) {
return;
}

if (checkType(node.typeAnnotation)) {
const suggest: TSESLint.ReportSuggestionArray<MessageIds> = [];
if (
node.parent.type === AST_NODE_TYPES.VariableDeclarator &&
!node.parent.id.typeAnnotation
) {
const { parent } = node;
suggest.push({
messageId: 'replaceObjectTypeAssertionWithAnnotation',
data: { cast: context.sourceCode.getText(node.typeAnnotation) },
fix: fixer => [
fixer.insertTextAfter(
parent.id,
`: ${context.sourceCode.getText(node.typeAnnotation)}`,
),
fixer.replaceText(
node,
getTextWithParentheses(context.sourceCode, node.expression),
),
],
});
}
suggest.push({
messageId: 'replaceObjectTypeAssertionWithSatisfies',
data: { cast: context.sourceCode.getText(node.typeAnnotation) },
fix: fixer => [
fixer.replaceText(
node,
getTextWithParentheses(context.sourceCode, node.expression),
),
fixer.insertTextAfter(
node,
` satisfies ${context.sourceCode.getText(node.typeAnnotation)}`,
),
],
});
const suggest = getSuggestions(
node,
'replaceObjectTypeAssertionWithAnnotation',
'replaceObjectTypeAssertionWithSatisfies',
);

context.report({
node,
Expand All @@ -262,22 +300,57 @@ export default createRule<Options, MessageIds>({
}
}

function checkExpressionForArrayAssertion(
node: AsExpressionOrTypeAssertion,
): void {
if (
options.assertionStyle === 'never' ||
options.arrayLiteralTypeAssertions === 'allow' ||
node.expression.type !== AST_NODE_TYPES.ArrayExpression
) {
return;
}

if (
options.arrayLiteralTypeAssertions === 'allow-as-parameter' &&
isAsParameter(node)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Testing]: This can be removed and no tests fail (&& isAsParameter(node)), I think it's important this would have a test that covers it.

) {
return;
}

if (checkType(node.typeAnnotation)) {
const suggest = getSuggestions(
node,
'replaceArrayTypeAssertionWithAnnotation',
'replaceArrayTypeAssertionWithSatisfies',
);

context.report({
node,
messageId: 'unexpectedArrayTypeAssertion',
suggest,
});
}
}

return {
TSAsExpression(node): void {
if (options.assertionStyle !== 'as') {
reportIncorrectAssertionType(node);
return;
}

checkExpression(node);
checkExpressionForObjectAssertion(node);
checkExpressionForArrayAssertion(node);
},
TSTypeAssertion(node): void {
if (options.assertionStyle !== 'angle-bracket') {
reportIncorrectAssertionType(node);
return;
}

checkExpression(node);
checkExpressionForObjectAssertion(node);
checkExpressionForArrayAssertion(node);
},
};
},
Expand Down
Loading
Loading