Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/rules/no-type-assertion-as.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Disallow the use of `as` type assertions and we suggest using `satisfies` instead to ensure that an expression conforms to a specific type without changing the type of the expression itself

## Fail

```ts
const events = request.body as TestEvent[];
const complexEvent = request.body as { type: string; payload: any };
```

## Pass

```ts
const newEvent = request.body satisfies AnotherEventType;
const complexEvent = request.body satisfies { type: string; payload: any };
```
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@checkdigit/eslint-plugin",
"version": "7.14.0",
"version": "7.15.0",
"description": "Check Digit eslint plugins",
"keywords": [
"eslint",
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import noEnum from './no-enum.ts';
import noSideEffects from './no-side-effects.ts';
import noRandomV4UUID from './no-random-v4-uuid.ts';
import noTestImport from './no-test-import.ts';
import noTypeAssertionAs from './no-type-assertion-as.ts';
import noUtil from './no-util.ts';
import noUuid from './no-uuid.ts';
import noWallabyComment from './no-wallaby-comment.ts';
Expand All @@ -56,6 +57,7 @@ const rules: Record<string, TSESLint.LooseRuleDefinition> = {
'no-test-import': noTestImport,
'no-wallaby-comment': noWallabyComment,
'no-side-effects': noSideEffects,
'no-type-assertion-as': noTypeAssertionAs,
'regular-expression-comment': regexComment,
'require-assert-predicate-rejects-throws': requireAssertPredicateRejectsThrows,
'object-literal-response': objectLiteralResponse,
Expand Down Expand Up @@ -94,6 +96,7 @@ const configs: Record<string, TSESLint.FlatConfig.Config[]> = {
'@checkdigit/require-ts-extension-imports-exports': 'error',
'@checkdigit/no-wallaby-comment': 'error',
'@checkdigit/no-side-effects': 'error',
'@checkdigit/no-type-assertion-as': 'error',
'@checkdigit/regular-expression-comment': 'error',
'@checkdigit/require-assert-predicate-rejects-throws': 'error',
'@checkdigit/object-literal-response': 'error',
Expand Down Expand Up @@ -129,6 +132,7 @@ const configs: Record<string, TSESLint.FlatConfig.Config[]> = {
'@checkdigit/require-ts-extension-imports-exports': 'error',
'@checkdigit/no-wallaby-comment': 'off',
'@checkdigit/no-side-effects': 'error',
'@checkdigit/no-type-assertion-as': 'error',
'@checkdigit/regular-expression-comment': 'error',
'@checkdigit/require-assert-predicate-rejects-throws': 'error',
'@checkdigit/object-literal-response': 'error',
Expand Down
130 changes: 130 additions & 0 deletions src/no-type-assertion-as.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// no-type-assertion-as.spec.ts

import rule, { ruleId } from './no-type-assertion-as';
import createTester from './ts-tester.test';

createTester().run(ruleId, rule, {
valid: [
{
name: 'Valid case without type assertion',
code: `const event = request.body;`,
},
{
name: 'Valid case with type annotation',
code: `const event: TestEvent = request.body;`,
},
{
name: 'Valid case with satisfies',
code: `const event = request.body satisfies TestEvent;`,
},
{
name: 'Valid case with satisfies and different type',
code: `const newEvent = request.body satisfies AnotherEventType;`,
},
{
name: 'Valid case with satisfies and complex type',
code: `const complexEvent = request.body satisfies { type: string; payload: any };`,
},
{
name: 'Valid case with satisfies and array type',
code: `const events = request.body satisfies TestEvent[];`,
},
],
invalid: [
{
name: 'Invalid case with as type assertion',
code: `const event = request.body as TestEvent;`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as type assertion and different type',
code: `const newEvent = request.body as AnotherEventType;`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as type assertion and complex type',
code: `const complexEvent = request.body as { type: string; payload: any };`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as type assertion and array type',
code: `const events = request.body as TestEvent[];`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as type assertion and union type',
code: `const result = response as SuccessResponse | ErrorResponse;`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as type assertion and intersection type',
code: `const result = response as SuccessResponse & ErrorResponse;`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as unknown type assertion',
code: `const value = someValue as unknown;`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as unknown as type assertion',
code: `const value = someValue as unknown as SomeType;`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as unknown as part of union type assertion',
code: `const result = response as unknown | SomeType;`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as unknown as part of intersection type assertion',
code: `const result = response as unknown & SomeType`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with nested as type assertion',
code: `const nested = (request.body as TestEvent) as AnotherEventType;`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as type assertion in function parameter',
code: `function handleEvent(event: any) { const typedEvent = event as TestEvent; }`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as type assertion in return statement',
code: `function getEvent(): TestEvent { return request.body as TestEvent; }`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as type assertion in arrow function',
code: `const getEvent = (): TestEvent => request.body as TestEvent;`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as type assertion in class property',
code: `class EventProcessor { private event = request.body as TestEvent; }`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as type assertion in conditional expression',
code: `const event = condition ? (request.body as TestEvent) : (request.body as AnotherEventType);`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }, { messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as type assertion in template literal',
code: `const event = \`\${request.body as TestEvent}\`;`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with as type assertion in array destructuring',
code: `const [event] = [request.body as TestEvent];`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
{
name: 'Invalid case with double as type assertion (unknown to specific type)',
code: `const value = someValue as unknown as SomeType;`,
errors: [{ messageId: 'NO_AS_TYPE_ASSERTION' }],
},
],
});
57 changes: 57 additions & 0 deletions src/no-type-assertion-as.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// no-type-assertion-as.ts

/*
* Copyright (c) 2021-2025 Check Digit, LLC
*
* This code is licensed under the MIT license (see LICENSE.txt for details).
*/

import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils';

export const ruleId = 'no-as-type-assertion';
const NO_AS_TYPE_ASSERTION = 'NO_AS_TYPE_ASSERTION';

const createRule = ESLintUtils.RuleCreator((name) => name);

const rule: ESLintUtils.RuleModule<typeof NO_AS_TYPE_ASSERTION> = createRule({
name: ruleId,
meta: {
type: 'problem',
docs: {
description: 'Disallow the use of `as` type assertions and suggest using `satisfies` instead',
},
schema: [],
messages: {
[NO_AS_TYPE_ASSERTION]: 'Avoid using `as` type assertions. Use `satisfies` instead.',
},
},
defaultOptions: [],
create(context) {
const parserServices = ESLintUtils.getParserServices(context);
const checker = parserServices.program.getTypeChecker();

return {
TSAsExpression(node: TSESTree.TSAsExpression) {
if (node.parent.type === AST_NODE_TYPES.TSAsExpression) {
return;
}

const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node.expression);
const originalType = checker.getTypeAtLocation(tsNode);
const targetType = checker.getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(node.typeAnnotation));

// Ensure the types are not assignable
if (!checker.isTypeAssignableTo(originalType, targetType)) {
return;
}

context.report({
node,
messageId: NO_AS_TYPE_ASSERTION,
});
},
};
},
});

export default rule;
Loading