diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx new file mode 100644 index 000000000000..53c1e51ee970 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx @@ -0,0 +1,63 @@ +--- +description: 'Disallow type assertions that narrow a type.' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-unsafe-type-assertion** for documentation. + +Type assertions are a way to tell TypeScript what the type of a value is. This can be useful but also unsafe if you use type assertions to narrow down a type. + +This rule forbids using type assertions to narrow a type, as this bypasses TypeScript's type-checking. Type assertions that broaden a type are safe because TypeScript essentially knows _less_ about a type. + +Instead of using type assertions to narrow a type, it's better to rely on type guards, which help avoid potential runtime errors caused by unsafe type assertions. + +## Examples + + + + +```ts +function f() { + return Math.random() < 0.5 ? 42 : 'oops'; +} + +const z = f() as number; + +const items = [1, '2', 3, '4']; + +const number = items[0] as number; +``` + + + + +```ts +function f() { + return Math.random() < 0.5 ? 42 : 'oops'; +} + +const z = f() as number | string | boolean; + +const items = [1, '2', 3, '4']; + +const number = items[0] as number | string | undefined; +``` + + + + +## When Not To Use It + +If your codebase has many unsafe type assertions, then it may be difficult to enable this rule. +It may be easier to skip the `no-unsafe-*` rules pending increasing type safety in unsafe areas of your project. +You might consider using [ESLint disable comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for those specific situations instead of completely disabling this rule. + +If your project frequently stubs objects in test files, the rule may trigger a lot of reports. Consider disabling the rule for such files to reduce frequent warnings. + +## Further Reading + +- More on TypeScript's [type assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 107f369260ec..0bb7560c49a6 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -106,6 +106,7 @@ export = { '@typescript-eslint/no-unsafe-function-type': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', + '@typescript-eslint/no-unsafe-type-assertion': 'error', '@typescript-eslint/no-unsafe-unary-minus': 'error', 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'error', diff --git a/packages/eslint-plugin/src/configs/disable-type-checked.ts b/packages/eslint-plugin/src/configs/disable-type-checked.ts index 7cf867b382f2..4e25bccd8405 100644 --- a/packages/eslint-plugin/src/configs/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts @@ -40,6 +40,7 @@ export = { '@typescript-eslint/no-unsafe-enum-comparison': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-type-assertion': 'off', '@typescript-eslint/no-unsafe-unary-minus': 'off', '@typescript-eslint/non-nullable-type-assertion-style': 'off', '@typescript-eslint/only-throw-error': 'off', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 72c62f3e0122..843e3d7f9a32 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -83,6 +83,7 @@ import noUnsafeEnumComparison from './no-unsafe-enum-comparison'; import noUnsafeFunctionType from './no-unsafe-function-type'; import noUnsafeMemberAccess from './no-unsafe-member-access'; import noUnsafeReturn from './no-unsafe-return'; +import noUnsafeTypeAssertion from './no-unsafe-type-assertion'; import noUnsafeUnaryMinus from './no-unsafe-unary-minus'; import noUnusedExpressions from './no-unused-expressions'; import noUnusedVars from './no-unused-vars'; @@ -213,6 +214,7 @@ const rules = { 'no-unsafe-function-type': noUnsafeFunctionType, 'no-unsafe-member-access': noUnsafeMemberAccess, 'no-unsafe-return': noUnsafeReturn, + 'no-unsafe-type-assertion': noUnsafeTypeAssertion, 'no-unsafe-unary-minus': noUnsafeUnaryMinus, 'no-unused-expressions': noUnusedExpressions, 'no-unused-vars': noUnusedVars, diff --git a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts new file mode 100644 index 000000000000..06c3dd44615a --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts @@ -0,0 +1,146 @@ +import type { TSESTree } from '@typescript-eslint/utils'; + +import * as tsutils from 'ts-api-utils'; +import * as ts from 'typescript'; + +import { + createRule, + getConstrainedTypeAtLocation, + getParserServices, + isTypeAnyType, + isTypeUnknownType, + isUnsafeAssignment, +} from '../util'; + +export default createRule({ + name: 'no-unsafe-type-assertion', + meta: { + type: 'problem', + docs: { + description: 'Disallow type assertions that narrow a type', + requiresTypeChecking: true, + }, + messages: { + unsafeOfAnyTypeAssertion: + 'Unsafe cast from {{type}} detected: consider using type guards or a safer cast.', + unsafeToAnyTypeAssertion: + 'Unsafe cast to {{type}} detected: consider using a more specific type to ensure safety.', + unsafeTypeAssertion: + "Unsafe type assertion: type '{{type}}' is more narrow than the original type.", + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const services = getParserServices(context); + const checker = services.program.getTypeChecker(); + + function getAnyTypeName(type: ts.Type): string { + return tsutils.isIntrinsicErrorType(type) ? 'error typed' : '`any`'; + } + + function isObjectLiteralType(type: ts.Type): boolean { + return ( + tsutils.isObjectType(type) && + tsutils.isObjectFlagSet(type, ts.ObjectFlags.ObjectLiteral) + ); + } + + function checkExpression( + node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, + ): void { + const expressionType = getConstrainedTypeAtLocation( + services, + node.expression, + ); + const assertedType = getConstrainedTypeAtLocation( + services, + node.typeAnnotation, + ); + + if (expressionType === assertedType) { + return; + } + + // handle cases when casting unknown ==> any. + if (isTypeAnyType(assertedType) && isTypeUnknownType(expressionType)) { + context.report({ + node, + messageId: 'unsafeToAnyTypeAssertion', + data: { + type: '`any`', + }, + }); + + return; + } + + const unsafeExpressionAny = isUnsafeAssignment( + expressionType, + assertedType, + checker, + node.expression, + ); + + if (unsafeExpressionAny) { + context.report({ + node, + messageId: 'unsafeOfAnyTypeAssertion', + data: { + type: getAnyTypeName(unsafeExpressionAny.sender), + }, + }); + + return; + } + + const unsafeAssertedAny = isUnsafeAssignment( + assertedType, + expressionType, + checker, + node.typeAnnotation, + ); + + if (unsafeAssertedAny) { + context.report({ + node, + messageId: 'unsafeToAnyTypeAssertion', + data: { + type: getAnyTypeName(unsafeAssertedAny.sender), + }, + }); + + return; + } + + // Use the widened type in case of an object literal so `isTypeAssignableTo()` + // won't fail on excess property check. + const nodeWidenedType = isObjectLiteralType(expressionType) + ? checker.getWidenedType(expressionType) + : expressionType; + + const isAssertionSafe = checker.isTypeAssignableTo( + nodeWidenedType, + assertedType, + ); + + if (!isAssertionSafe) { + context.report({ + node, + messageId: 'unsafeTypeAssertion', + data: { + type: checker.typeToString(assertedType), + }, + }); + } + } + + return { + 'TSAsExpression, TSTypeAssertion'( + node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, + ): void { + checkExpression(node); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-type-assertion.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-type-assertion.shot new file mode 100644 index 000000000000..bcc2b982cc4d --- /dev/null +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-type-assertion.shot @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Validating rule docs no-unsafe-type-assertion.mdx code examples ESLint output 1`] = ` +"Incorrect + +function f() { + return Math.random() < 0.5 ? 42 : 'oops'; +} + +const z = f() as number; + ~~~~~~~~~~~~~ Unsafe type assertion: type 'number' is more narrow than the original type. + +const items = [1, '2', 3, '4']; + +const number = items[0] as number; + ~~~~~~~~~~~~~~~~~~ Unsafe type assertion: type 'number' is more narrow than the original type. +" +`; + +exports[`Validating rule docs no-unsafe-type-assertion.mdx code examples ESLint output 2`] = ` +"Correct + +function f() { + return Math.random() < 0.5 ? 42 : 'oops'; +} + +const z = f() as number | string | boolean; + +const items = [1, '2', 3, '4']; + +const number = items[0] as number | string | undefined; +" +`; diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts new file mode 100644 index 000000000000..aa73bc2dae4d --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts @@ -0,0 +1,1043 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-unsafe-type-assertion'; +import { getFixturesRootDir } from '../RuleTester'; + +const rootPath = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: rootPath, + }, + }, +}); + +describe('basic assertions', () => { + ruleTester.run('no-unsafe-type-assertion', rule, { + valid: [ + ` +declare const a: string; +a as string | number; + `, + ` +declare const a: string; +a; + `, + ` +declare const a: string; +a as string | number as string | number | boolean; + `, + ` +declare const a: string; +a as string; + `, + ` +declare const a: { hello: 'world' }; +a as { hello: string }; + `, + ` +'hello' as const; + `, + ` +function foo(a: T) { + return a as T | number; +} + `, + ], + invalid: [ + { + code: ` +declare const a: string | number; +a as string; + `, + errors: [ + { + column: 1, + data: { + type: 'string', + }, + endColumn: 12, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: string | number; +a satisfies string as string; + `, + errors: [ + { + column: 1, + data: { + type: 'string', + }, + endColumn: 29, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: string | number; +a; + `, + errors: [ + { + column: 1, + data: { + type: 'string', + }, + endColumn: 10, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: string | undefined; +a as string | boolean; + `, + errors: [ + { + column: 1, + data: { + type: 'string | boolean', + }, + endColumn: 22, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + // multiple failures + { + code: ` +declare const a: string; +a as 'foo' as 'bar'; + `, + errors: [ + { + column: 1, + data: { + type: '"bar"', + }, + endColumn: 20, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + { + column: 1, + data: { + type: '"foo"', + }, + endColumn: 11, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + // type constraint + { + code: ` +function foo(a: T) { + return a as true; +} + `, + errors: [ + { + column: 10, + data: { + type: 'true', + }, + endColumn: 19, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + // long/complex asserted type + { + code: ` +declare const a: string; +a as Omit>, 'foo'>; + `, + errors: [ + { + column: 1, + data: { + type: 'Omit>, "foo">', + }, + endColumn: 69, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const foo: readonly number[]; +const bar = foo as number[]; + `, + errors: [ + { + column: 13, + data: { + type: 'number[]', + }, + endColumn: 28, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + ], + }); +}); + +describe('any assertions', () => { + ruleTester.run('no-unsafe-type-assertion', rule, { + valid: [ + ` +declare const _any_: any; +_any_ as any; + `, + ` +declare const _any_: any; +_any_ as unknown; + `, + ], + invalid: [ + { + code: ` +declare const _any_: any; +_any_ as string; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 16, + endLine: 3, + line: 3, + messageId: 'unsafeOfAnyTypeAssertion', + }, + ], + }, + { + code: ` +declare const _unknown_: unknown; +_unknown_ as any; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 17, + endLine: 3, + line: 3, + messageId: 'unsafeToAnyTypeAssertion', + }, + ], + }, + { + code: ` +declare const _any_: any; +_any_ as Function; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 18, + endLine: 3, + line: 3, + messageId: 'unsafeOfAnyTypeAssertion', + }, + ], + }, + { + code: ` +declare const _any_: any; +_any_ as never; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 15, + endLine: 3, + line: 3, + messageId: 'unsafeOfAnyTypeAssertion', + }, + ], + }, + { + code: ` +'foo' as any; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 13, + endLine: 2, + line: 2, + messageId: 'unsafeToAnyTypeAssertion', + }, + ], + }, + // an error type `any` + { + code: ` +const bar = foo as number; + `, + errors: [ + { + column: 13, + data: { + type: 'error typed', + }, + endColumn: 26, + endLine: 2, + line: 2, + messageId: 'unsafeOfAnyTypeAssertion', + }, + ], + }, + { + code: ` +const bar = 'foo' as errorType; + `, + errors: [ + { + column: 13, + data: { + type: 'error typed', + }, + endColumn: 31, + endLine: 2, + line: 2, + messageId: 'unsafeToAnyTypeAssertion', + }, + ], + }, + ], + }); +}); + +describe('never assertions', () => { + ruleTester.run('no-unsafe-type-assertion', rule, { + valid: [ + ` +declare const _never_: never; +_never_ as never; + `, + ` +declare const _never_: never; +_never_ as unknown; + `, + ], + invalid: [ + { + code: ` +declare const _never_: never; +_never_ as any; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 15, + endLine: 3, + line: 3, + messageId: 'unsafeToAnyTypeAssertion', + }, + ], + }, + { + code: ` +declare const _string_: string; +_string_ as never; + `, + errors: [ + { + column: 1, + endColumn: 18, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + ], + }); +}); + +describe('function assertions', () => { + ruleTester.run('no-unsafe-type-assertion', rule, { + valid: [ + ` +declare const _function_: Function; +_function_ as Function; + `, + ` +declare const _function_: Function; +_function_ as unknown; + `, + ], + invalid: [ + { + code: ` +declare const _function_: Function; +_function_ as () => void; + `, + errors: [ + { + column: 1, + endColumn: 25, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const _function_: Function; +_function_ as any; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 18, + endLine: 3, + line: 3, + messageId: 'unsafeToAnyTypeAssertion', + }, + ], + }, + { + code: ` +declare const _function_: Function; +_function_ as never; + `, + errors: [ + { + column: 1, + endColumn: 20, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + ], + }); +}); + +describe('object assertions', () => { + ruleTester.run('no-unsafe-type-assertion', rule, { + valid: [ + ` +// additional properties should be allowed +export const foo = { bar: 1, bazz: 1 } as { + bar: number; +}; + `, + ` +declare const a: { hello: string } & { world: string }; +a as { hello: string }; + `, + ` +declare const a: { hello: any }; +a as { hello: unknown }; + `, + ` +declare const a: { hello: string }; +a as { hello?: string }; + `, + ` +declare const a: { hello: string }; +a satisfies Record as { hello?: string }; + `, + ], + invalid: [ + { + code: ` +var foo = {} as { + bar: number; + bas: string; +}; + `, + errors: [ + { + column: 11, + data: { + type: '{ bar: number; bas: string; }', + }, + endColumn: 2, + endLine: 5, + line: 2, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: { hello: string }; +a satisfies Record as { hello: string; world: string }; + `, + errors: [ + { + column: 1, + data: { + type: '{ hello: string; world: string; }', + }, + endColumn: 71, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: { hello?: string }; +a as { hello: string }; + `, + errors: [ + { + column: 1, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + ], + }); +}); + +describe('array assertions', () => { + ruleTester.run('no-unsafe-type-assertion', rule, { + valid: [ + ` +declare const a: string[]; +a as (string | number)[]; + `, + ` +declare const a: number[]; +a as unknown[]; + `, + ` +declare const a: { hello: 'world'; foo: 'bar' }[]; +a as { hello: 'world' }[]; + `, + ], + invalid: [ + { + code: ` +declare const a: (string | number)[]; +a as string[]; + `, + errors: [ + { + column: 1, + data: { + type: 'string[]', + }, + endColumn: 14, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: any[]; +a as number[]; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 14, + endLine: 3, + line: 3, + messageId: 'unsafeOfAnyTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: number[]; +a as any[]; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 11, + endLine: 3, + line: 3, + messageId: 'unsafeToAnyTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: unknown[]; +a as number[]; + `, + errors: [ + { + column: 1, + endColumn: 14, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: number[]; +a as never[]; + `, + errors: [ + { + column: 1, + endColumn: 13, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + ], + }); +}); + +describe('tuple assertions', () => { + ruleTester.run('no-unsafe-type-assertion', rule, { + valid: [ + ` +declare const a: [string]; +a as [string | number]; + `, + ` +declare const a: [string, number]; +a as [string, string | number]; + `, + ` +declare const a: [string]; +a as [unknown]; + `, + ` +declare const a: [{ hello: 'world'; foo: 'bar' }]; +a as [{ hello: 'world' }]; + `, + ], + invalid: [ + { + code: ` +declare const a: [string | number]; +a as [string]; + `, + errors: [ + { + column: 1, + data: { + type: '[string]', + }, + endColumn: 14, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: [string, number]; +a as [string, string]; + `, + errors: [ + { + column: 1, + data: { + type: '[string, string]', + }, + endColumn: 22, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: [string]; +a as [string, number]; + `, + errors: [ + { + column: 1, + data: { + type: '[string, number]', + }, + endColumn: 22, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: [string, number]; +a as [string]; + `, + errors: [ + { + column: 1, + data: { + type: '[string]', + }, + endColumn: 14, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: [any]; +a as [number]; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 14, + endLine: 3, + line: 3, + messageId: 'unsafeOfAnyTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: [number, any]; +a as [number, number]; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 22, + endLine: 3, + line: 3, + messageId: 'unsafeOfAnyTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: [number]; +a as [any]; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 11, + endLine: 3, + line: 3, + messageId: 'unsafeToAnyTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: [unknown]; +a as [number]; + `, + errors: [ + { + column: 1, + endColumn: 14, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: [number]; +a as [never]; + `, + errors: [ + { + column: 1, + endColumn: 13, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: [Promise]; +a as [Promise]; + `, + errors: [ + { + column: 1, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + ], + }); +}); + +describe('promise assertions', () => { + ruleTester.run('no-unsafe-type-assertion', rule, { + valid: [ + ` +declare const a: Promise; +a as Promise; + `, + ` +declare const a: Promise; +a as Promise; + `, + ` +declare const a: Promise<{ hello: 'world'; foo: 'bar' }>; +a as Promise<{ hello: 'world' }>; + `, + ` +declare const a: Promise; +a as Promise | string; + `, + ], + invalid: [ + { + code: ` +declare const a: Promise; +a as Promise; + `, + errors: [ + { + column: 1, + data: { + type: 'Promise', + }, + endColumn: 21, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: Promise; +a as Promise; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 21, + endLine: 3, + line: 3, + messageId: 'unsafeOfAnyTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: Promise; +a as Promise; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 18, + endLine: 3, + line: 3, + messageId: 'unsafeToAnyTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: Promise; +a as Promise; + `, + errors: [ + { + column: 1, + data: { + type: '`any`', + }, + endColumn: 20, + endLine: 3, + line: 3, + messageId: 'unsafeToAnyTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: Promise; +a as Promise; + `, + errors: [ + { + column: 1, + endColumn: 21, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: Promise; +a as Promise; + `, + errors: [ + { + column: 1, + endColumn: 20, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + ], + }); +}); + +describe('class assertions', () => { + ruleTester.run('no-unsafe-type-assertion', rule, { + valid: [ + ` +class Foo {} +declare const a: Foo; +a as Foo | number; + `, + ` +class Foo {} +class Bar {} +declare const a: Foo; +a as Bar; + `, + ` +class Foo { + hello() {} +} +class Bar {} +declare const a: Foo; +a as Bar; + `, + ` +class Foo { + hello() {} +} +class Bar extends Foo {} +declare const a: Bar; +a as Foo; + `, + ` +class Foo { + hello() {} +} +class Bar extends Foo {} +declare const a: Foo; +a as Bar; + `, + ], + invalid: [ + { + code: ` +class Foo { + hello() {} +} +class Bar extends Foo { + world() {} +} +declare const a: Foo; +a as Bar; + `, + errors: [ + { + column: 1, + endColumn: 9, + endLine: 9, + line: 9, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + ], + }); +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-unsafe-type-assertion.shot b/packages/eslint-plugin/tests/schema-snapshots/no-unsafe-type-assertion.shot new file mode 100644 index 000000000000..92a34949c0cb --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/no-unsafe-type-assertion.shot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes no-unsafe-type-assertion 1`] = ` +" +# SCHEMA: + +[] + + +# TYPES: + +/** No options declared */ +type Options = [];" +`; diff --git a/packages/typescript-eslint/src/configs/all.ts b/packages/typescript-eslint/src/configs/all.ts index 1c177f1943bf..4cfc2df7f640 100644 --- a/packages/typescript-eslint/src/configs/all.ts +++ b/packages/typescript-eslint/src/configs/all.ts @@ -120,6 +120,7 @@ export default ( '@typescript-eslint/no-unsafe-function-type': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', + '@typescript-eslint/no-unsafe-type-assertion': 'error', '@typescript-eslint/no-unsafe-unary-minus': 'error', 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'error', diff --git a/packages/typescript-eslint/src/configs/disable-type-checked.ts b/packages/typescript-eslint/src/configs/disable-type-checked.ts index b4c2afd20ec8..f1185e195ac5 100644 --- a/packages/typescript-eslint/src/configs/disable-type-checked.ts +++ b/packages/typescript-eslint/src/configs/disable-type-checked.ts @@ -47,6 +47,7 @@ export default ( '@typescript-eslint/no-unsafe-enum-comparison': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-type-assertion': 'off', '@typescript-eslint/no-unsafe-unary-minus': 'off', '@typescript-eslint/non-nullable-type-assertion-style': 'off', '@typescript-eslint/only-throw-error': 'off',