From 8f38cec6635c534a991578c011361d28905b9e1d Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 24 Sep 2024 22:48:41 +0300 Subject: [PATCH 01/25] initial implementation --- packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/no-unsafe-type-assertion.ts | 63 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 49d8bd67c5cb..35bc8055054f 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 @@ export default { '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..9957c8bbe96a --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts @@ -0,0 +1,63 @@ +import type { TSESTree } from '@typescript-eslint/utils'; + +import { + createRule, + getConstrainedTypeAtLocation, + getParserServices, + getTypeName, +} from '../util'; + +export default createRule({ + name: 'no-unsafe-type-assertion', + meta: { + type: 'problem', + docs: { + description: 'Disallow type assertions that widen a type', + requiresTypeChecking: true, + }, + messages: { + unsafeTypeAssertion: + 'Unsafe type assertion, type `{{type}}` is not assignable to type `{{asserted}}`', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const services = getParserServices(context); + const checker = services.program.getTypeChecker(); + + function checkExpression( + node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, + ): void { + const nodeType = getConstrainedTypeAtLocation(services, node.expression); + const assertedType = getConstrainedTypeAtLocation( + services, + node.typeAnnotation, + ); + + const isAssertionSafe = checker.isTypeAssignableTo( + nodeType, + assertedType, + ); + + if (!isAssertionSafe) { + context.report({ + node, + messageId: 'unsafeTypeAssertion', + data: { + type: getTypeName(checker, nodeType), + asserted: getTypeName(checker, assertedType), + }, + }); + } + } + + return { + 'TSAsExpression, TSTypeAssertion'( + node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, + ): void { + checkExpression(node); + }, + }; + }, +}); From 037978c899fe3029120abf5c67e7ce8c863f62d3 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 24 Sep 2024 22:48:51 +0300 Subject: [PATCH 02/25] tests --- .../rules/no-unsafe-type-assertion.test.ts | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts 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..893782133009 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts @@ -0,0 +1,165 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-unsafe-type-assertion'; +import { getFixturesRootDir } from '../RuleTester'; + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + project: './tsconfig.noImplicitThis.json', + projectService: false, + tsconfigRootDir: getFixturesRootDir(), + }, + }, +}); + +ruleTester.run('no-unsafe-member-access', rule, { + valid: [ + ` +declare const a: string; +const b = a as string | number; + `, + ` +declare const a: string; +const b = a as unknown; + `, + ` +declare const a: () => boolean; +const b = a() as boolean | number; + `, + ` +declare const a: string; +const b = a as string; + `, + ` +declare const a: string; +const b = a; + `, + ` +declare const a: string; +const b = a; + `, + ` +declare const a: () => boolean; +const b = a(); + `, + ` +declare const a: string; +const b = a; + `, + ], + invalid: [ + { + code: ` +declare const a: string | number; +const b = a as string; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 11, + endColumn: 22, + data: { + type: 'string | number', + asserted: 'string', + }, + }, + ], + }, + { + code: ` +declare const a: string | number; +const b = a; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 11, + endColumn: 20, + data: { + type: 'string | number', + asserted: 'string', + }, + }, + ], + }, + { + code: ` +declare const a: string; +const b = a as unknown as number; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 11, + endColumn: 33, + data: { + type: 'unknown', + asserted: 'number', + }, + }, + ], + }, + { + code: ` +declare const a: string | undefined; +const b = a as string | boolean; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 11, + endColumn: 32, + data: { + type: 'string | undefined', + asserted: 'string | boolean', + }, + }, + ], + }, + { + code: ` +function f(t: number | string) { + return t as number | boolean; +} + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 10, + endColumn: 31, + data: { + type: 'string | number', + asserted: 'number | boolean', + }, + }, + ], + }, + { + code: ` +interface Foo { + bar: number; + bas: string; +} +var foo = {} as Foo; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 6, + column: 11, + endColumn: 20, + data: { + type: '{}', + asserted: 'Foo', + }, + }, + ], + }, + ], +}); From a50625bf48ef63570ccbb1cad2c14144331b873d Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 24 Sep 2024 22:48:57 +0300 Subject: [PATCH 03/25] docs --- .../docs/rules/no-unsafe-type-assertion.mdx | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx 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..77aa35682ca8 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx @@ -0,0 +1,59 @@ +--- +description: 'Disallow type assertions that widen 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. + +This rule forbids using type assertions to narrow a type, as it can lead to unsafe assumptions that bypass TypeScript’s type-checking. + +Instead, it's better to rely on type guards to ensure type safety and 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. + +## Further Reading + +- More on https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions From 15ce7f8729ba3064c052b74a8496112ad0a1d126 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 24 Sep 2024 22:53:55 +0300 Subject: [PATCH 04/25] more tests --- .../rules/no-unsafe-type-assertion.test.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) 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 index 893782133009..829efd57515f 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts @@ -146,12 +146,13 @@ interface Foo { bar: number; bas: string; } + var foo = {} as Foo; `, errors: [ { messageId: 'unsafeTypeAssertion', - line: 6, + line: 7, column: 11, endColumn: 20, data: { @@ -161,5 +162,27 @@ var foo = {} as Foo; }, ], }, + { + code: ` +interface Foo { + bar: number; +} + +// no additional properties are allowed +export const foo = { bar: 1, bazz: 1 } as Foo; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 7, + column: 20, + endColumn: 46, + data: { + type: '{ bar: number; bazz: number; }', + asserted: 'Foo', + }, + }, + ], + }, ], }); From 7c44606c504a5cf2f01e95cf21dc570e823b0d5e Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 24 Sep 2024 23:07:20 +0300 Subject: [PATCH 05/25] use checker.typeToString() over getTypeName() --- .../docs/rules/no-unsafe-type-assertion.mdx | 6 +- packages/eslint-plugin/src/configs/all.ts | 1 + .../src/configs/disable-type-checked.ts | 1 + .../src/rules/no-unsafe-type-assertion.ts | 7 +- .../no-unsafe-type-assertion.shot | 33 ++ .../rules/no-unsafe-type-assertion.test.ts | 395 +++++++++++++++++- .../no-unsafe-type-assertion.shot | 14 + packages/typescript-eslint/src/configs/all.ts | 1 + .../src/configs/disable-type-checked.ts | 1 + 9 files changed, 441 insertions(+), 18 deletions(-) create mode 100644 packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-type-assertion.shot create mode 100644 packages/eslint-plugin/tests/schema-snapshots/no-unsafe-type-assertion.shot diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx index 77aa35682ca8..f57fbf75ec86 100644 --- a/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx +++ b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx @@ -1,5 +1,5 @@ --- -description: 'Disallow type assertions that widen a type' +description: 'Disallow type assertions that widen a type.' --- import Tabs from '@theme/Tabs'; @@ -50,10 +50,12 @@ 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. +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. +This rule requires TypeScript v5.4 or above; you should not use it if your TypeScript version is lower. + ## Further Reading - More on 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/no-unsafe-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts index 9957c8bbe96a..5ed61ce071b3 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts @@ -4,7 +4,6 @@ import { createRule, getConstrainedTypeAtLocation, getParserServices, - getTypeName, } from '../util'; export default createRule({ @@ -17,7 +16,7 @@ export default createRule({ }, messages: { unsafeTypeAssertion: - 'Unsafe type assertion, type `{{type}}` is not assignable to type `{{asserted}}`', + 'Unsafe type assertion: type `{{type}}` is not assignable to type `{{asserted}}`', }, schema: [], }, @@ -45,8 +44,8 @@ export default createRule({ node, messageId: 'unsafeTypeAssertion', data: { - type: getTypeName(checker, nodeType), - asserted: getTypeName(checker, assertedType), + type: checker.typeToString(nodeType), + asserted: checker.typeToString(assertedType), }, }); } 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..61e77619c588 --- /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 \`42 | "oops"\` is not assignable to type \`number\` + +const items = [1, '2', 3, '4']; + +const number = items[0] as number; + ~~~~~~~~~~~~~~~~~~ Unsafe type assertion: type \`string | number\` is not assignable to type \`number\` +" +`; + +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 index 829efd57515f..71eeda56d8c6 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts @@ -25,7 +25,7 @@ const b = a as unknown; `, ` declare const a: () => boolean; -const b = a() as boolean | number; +const b = a() as boolean | number | string; `, ` declare const a: string; @@ -41,12 +41,24 @@ const b = a; `, ` declare const a: () => boolean; -const b = a(); +const b = a(); `, ` declare const a: string; const b = a; `, + ` +declare const foo: () => string | undefined; +foo()!; + `, + ` +declare const foo = 'hello' as const; +foo() as string; + `, + ` +declare const foo: { bar?: { bazz: string } }; +(foo.bar as { bazz: string | boolean } | undefined)?.bazz; + `, ], invalid: [ { @@ -69,6 +81,235 @@ const b = a as string; }, { code: ` +declare const a: string; +const b = a as unknown as number; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 11, + endColumn: 33, + data: { + type: 'unknown', + asserted: 'number', + }, + }, + ], + }, + { + code: ` +declare const a: string | undefined; +const b = a as string | boolean; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 11, + endColumn: 32, + data: { + type: 'string | undefined', + asserted: 'string | boolean', + }, + }, + ], + }, + { + code: ` +function f(t: number | string) { + return t as number | boolean; +} + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 10, + endColumn: 31, + data: { + type: 'string | number', + asserted: 'number | boolean', + }, + }, + ], + }, + { + code: ` +interface Foo { + bar: number; + bas: string; +} + +var foo = {} as Foo; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 7, + column: 11, + endColumn: 20, + data: { + type: '{}', + asserted: 'Foo', + }, + }, + ], + }, + { + code: ` +interface Foo { + bar: number; +} + +// no additional properties are allowed +export const foo = { bar: 1, bazz: 1 } as Foo; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 7, + column: 20, + endColumn: 46, + data: { + type: '{ bar: number; bazz: number; }', + asserted: 'Foo', + }, + }, + ], + }, + { + code: ` +declare const foo: string | number; +const bar = foo as string | boolean as string | null; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 13, + endColumn: 53, + data: { + type: 'string | boolean', + asserted: 'string | null', + }, + }, + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 13, + endColumn: 36, + data: { + type: 'string | number', + asserted: 'string | boolean', + }, + }, + ], + }, + { + code: ` +declare const foo: { bar?: { bazz: string } }; +(foo.bar as { bazz: string | boolean }).bazz; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 2, + endColumn: 39, + data: { + type: '{ bazz: string; } | undefined', + asserted: '{ bazz: string | boolean; }', + }, + }, + ], + }, + { + code: ` +declare const foo: 'hello' | 'world'; +const bar = foo as 'hello'; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 13, + endColumn: 27, + data: { + type: '"hello" | "world"', + asserted: '"hello"', + }, + }, + ], + }, + { + code: ` +interface Foo { + type: 'foo'; +} + +interface Bar { + type: 'bar'; +} + +type Bazz = Foo | Bar; + +declare const foo: Bazz; +const bar = foo as Foo; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 13, + column: 13, + endColumn: 23, + data: { + type: 'Bazz', + asserted: 'Foo', + }, + }, + ], + }, + { + code: ` +type Foo = Readonly>; + +declare const foo: {}; +const bar = foo as Foo; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 5, + column: 13, + endColumn: 23, + data: { + type: '{}', + asserted: 'Readonly>', + }, + }, + ], + }, + { + code: ` +declare const foo: readonly number[]; +const bar = foo as number[]; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 13, + endColumn: 28, + data: { + type: 'readonly number[]', + asserted: 'number[]', + }, + }, + ], + }, + { + code: ` declare const a: string | number; const b = a; `, @@ -88,14 +329,14 @@ const b = a; { code: ` declare const a: string; -const b = a as unknown as number; +const b = (a); `, errors: [ { messageId: 'unsafeTypeAssertion', line: 3, column: 11, - endColumn: 33, + endColumn: 31, data: { type: 'unknown', asserted: 'number', @@ -106,14 +347,14 @@ const b = a as unknown as number; { code: ` declare const a: string | undefined; -const b = a as string | boolean; +const b = a; `, errors: [ { messageId: 'unsafeTypeAssertion', line: 3, column: 11, - endColumn: 32, + endColumn: 30, data: { type: 'string | undefined', asserted: 'string | boolean', @@ -124,7 +365,7 @@ const b = a as string | boolean; { code: ` function f(t: number | string) { - return t as number | boolean; + return t; } `, errors: [ @@ -132,7 +373,7 @@ function f(t: number | string) { messageId: 'unsafeTypeAssertion', line: 3, column: 10, - endColumn: 31, + endColumn: 29, data: { type: 'string | number', asserted: 'number | boolean', @@ -147,14 +388,14 @@ interface Foo { bas: string; } -var foo = {} as Foo; +var foo = {}; `, errors: [ { messageId: 'unsafeTypeAssertion', line: 7, column: 11, - endColumn: 20, + endColumn: 18, data: { type: '{}', asserted: 'Foo', @@ -169,14 +410,14 @@ interface Foo { } // no additional properties are allowed -export const foo = { bar: 1, bazz: 1 } as Foo; +export const foo = { bar: 1, bazz: 1 }; `, errors: [ { messageId: 'unsafeTypeAssertion', line: 7, column: 20, - endColumn: 46, + endColumn: 44, data: { type: '{ bar: number; bazz: number; }', asserted: 'Foo', @@ -184,5 +425,135 @@ export const foo = { bar: 1, bazz: 1 } as Foo; }, ], }, + { + code: ` +declare const foo: string | number; +const bar = (foo); + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 13, + endColumn: 51, + data: { + type: 'string | boolean', + asserted: 'string | null', + }, + }, + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 29, + endColumn: 50, + data: { + type: 'string | number', + asserted: 'string | boolean', + }, + }, + ], + }, + { + code: ` +declare const foo: { bar?: { bazz: string } }; +(<{ bazz: string | boolean }>foo.bar).bazz; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 2, + endColumn: 37, + data: { + type: '{ bazz: string; } | undefined', + asserted: '{ bazz: string | boolean; }', + }, + }, + ], + }, + { + code: ` +declare const foo: 'hello' | 'world'; +const bar = <'hello'>foo; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 13, + endColumn: 25, + data: { + type: '"hello" | "world"', + asserted: '"hello"', + }, + }, + ], + }, + { + code: ` +interface Foo { + type: 'foo'; +} + +interface Bar { + type: 'bar'; +} + +type Bazz = Foo | Bar; + +declare const foo: Bazz; +const bar = foo; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 13, + column: 13, + endColumn: 21, + data: { + type: 'Bazz', + asserted: 'Foo', + }, + }, + ], + }, + { + code: ` +type Foo = Readonly>; + +declare const foo: {}; +const bar = foo; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 5, + column: 13, + endColumn: 21, + data: { + type: '{}', + asserted: 'Readonly>', + }, + }, + ], + }, + { + code: ` +declare const foo: readonly number[]; +const bar = foo; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 13, + endColumn: 26, + data: { + type: 'readonly number[]', + asserted: 'number[]', + }, + }, + ], + }, ], }); 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', From cc4ea8be1480f7a0463b28be5a58dfb4f868c4b2 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Wed, 25 Sep 2024 22:54:25 +0300 Subject: [PATCH 06/25] use link --- packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx index f57fbf75ec86..1f333434f6ec 100644 --- a/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx +++ b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx @@ -58,4 +58,4 @@ This rule requires TypeScript v5.4 or above; you should not use it if your TypeS ## Further Reading -- More on https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions +- More on TypeScript's [type assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) From e31cf39a65ae3a39b3765c05da48e6369bedd3ea Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Wed, 25 Sep 2024 22:58:13 +0300 Subject: [PATCH 07/25] oops --- .../tests/rules/no-unsafe-type-assertion.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 index 71eeda56d8c6..d9ab5e9a36ca 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts @@ -3,17 +3,18 @@ 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.noImplicitThis.json', - projectService: false, - tsconfigRootDir: getFixturesRootDir(), + tsconfigRootDir: rootPath, + project: './tsconfig.json', }, }, }); -ruleTester.run('no-unsafe-member-access', rule, { +ruleTester.run('no-unsafe-type-assertion', rule, { valid: [ ` declare const a: string; From 8a8965f900d7bd0813bf486b327bd31d0fb00bd1 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sat, 28 Sep 2024 16:31:26 +0300 Subject: [PATCH 08/25] add tests --- .../rules/no-unsafe-type-assertion.test.ts | 372 +++++++++++++++++- 1 file changed, 365 insertions(+), 7 deletions(-) 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 index d9ab5e9a36ca..c7d2d072f909 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts @@ -25,14 +25,81 @@ declare const a: string; const b = a as unknown; `, ` +declare const a: string; +const b = a as any; + `, + ` +declare const a: string; +const b = a as string | number as string | number | boolean; + `, + ` +declare const a: string; +const b = a as any as number; + `, + ` declare const a: () => boolean; -const b = a() as boolean | number | string; +const b = a() as boolean | number; + `, + ` +declare const a: () => boolean; +const b = a() as boolean | number as boolean | number | string; `, ` declare const a: string; const b = a as string; `, ` +declare const a: () => string; +const b = a() as string; + `, + ` +declare const a: () => string; +const b = a as (() => string) | (() => number); + `, + ` +declare const a: () => string; +const b = a as (() => string) | ((x: number) => string); + `, + ` +declare const a: () => string; +const b = a as () => string | number; + `, + ` +declare const a: { hello: 'world' }; +const b = a as { hello: string }; + `, + ` +declare const foo = 'hello' as const; +foo() as string; + `, + ` +declare const foo: () => string | undefined; +foo()!; + `, + ` +declare const foo: { bar?: { bazz: string } }; +(foo.bar as { bazz: string | boolean } | undefined)?.bazz; + `, + ` +function foo(a: string) { + return a as string | number; +} + `, + ` +function foo(a: T) { + return a as boolean | number; +} + `, + ` +function foo(a: T) { + return a as T | number; +} + `, + ` +declare const a: { hello: string } & { world: string }; +const b = a as { hello: string }; + `, + ` declare const a: string; const b = a; `, @@ -41,24 +108,75 @@ declare const a: string; const b = a; `, ` +declare const a: string; +const b = a; + `, + ` +declare const a: string; +const b = (a); + `, + ` +declare const a: string; +const b = (a); + `, + ` declare const a: () => boolean; -const b = a(); +const b = a(); + `, + ` +declare const a: () => boolean; +const b = boolean | number | string(a()); `, ` declare const a: string; const b = a; `, ` -declare const foo: () => string | undefined; -foo()!; +declare const a: () => string; +const b = a(); `, ` -declare const foo = 'hello' as const; -foo() as string; +declare const a: () => string; +const b = <(() => string) | (() => number)>a; + `, + ` +declare const a: () => string; +const b = <(() => string) | ((x: number) => string)>a; + `, + ` +declare const a: () => string; +const b = <() => string | number>a; + `, + ` +declare const a: { hello: 'world' }; +const b = <{ hello: string }>a; + `, + ` +declare const foo = 'hello'; +foo(); `, ` declare const foo: { bar?: { bazz: string } }; -(foo.bar as { bazz: string | boolean } | undefined)?.bazz; +(<{ bazz: string | boolean } | undefined>foo.bar)?.bazz; + `, + ` +function foo(a: string) { + return a; +} + `, + ` +function foo(a: T) { + return a; +} + `, + ` +function foo(a: T) { + return a; +} + `, + ` +declare const a: { hello: string } & { world: string }; +const b = <{ hello: string }>a; `, ], invalid: [ @@ -118,6 +236,52 @@ const b = a as string | boolean; }, { code: ` +declare const a: string; +const b = a as string | boolean as boolean; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 11, + endColumn: 43, + data: { + type: 'string | boolean', + asserted: 'boolean', + }, + }, + ], + }, + { + code: ` +declare const a: string; +const b = a as 'foo' as 'bar'; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 11, + endColumn: 30, + data: { + type: '"foo"', + asserted: '"bar"', + }, + }, + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 11, + endColumn: 21, + data: { + type: 'string', + asserted: '"foo"', + }, + }, + ], + }, + { + code: ` function f(t: number | string) { return t as number | boolean; } @@ -137,6 +301,62 @@ function f(t: number | string) { }, { code: ` +function f(t: T) { + return t as number | boolean; +} + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 10, + endColumn: 31, + data: { + type: 'string | number', + asserted: 'number | boolean', + }, + }, + ], + }, + { + code: ` +function f(t: T) { + return t as Omit; +} + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 10, + endColumn: 30, + data: { + type: 'string | number', + asserted: 'Omit', + }, + }, + ], + }, + { + code: ` +declare const a: () => string | boolean; +const b = a as () => string | number; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 11, + endColumn: 37, + data: { + type: '() => string | boolean', + asserted: '() => string | number', + }, + }, + ], + }, + { + code: ` interface Foo { bar: number; bas: string; @@ -311,6 +531,24 @@ const bar = foo as number[]; }, { code: ` +declare const foo: { hello: string } & { world: string }; +const bar = foo as { hello: string; world: 'world' }; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 13, + endColumn: 53, + data: { + type: '{ hello: string; } & { world: string; }', + asserted: '{ hello: string; world: "world"; }', + }, + }, + ], + }, + { + code: ` declare const a: string | number; const b = a; `, @@ -365,6 +603,52 @@ const b = a; }, { code: ` +declare const a: string; +const b = (a); + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 11, + endColumn: 41, + data: { + type: 'string | boolean', + asserted: 'boolean', + }, + }, + ], + }, + { + code: ` +declare const a: string; +const b = <'bar'>(<'foo'>a); + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 11, + endColumn: 28, + data: { + type: '"foo"', + asserted: '"bar"', + }, + }, + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 19, + endColumn: 27, + data: { + type: 'string', + asserted: '"foo"', + }, + }, + ], + }, + { + code: ` function f(t: number | string) { return t; } @@ -384,6 +668,62 @@ function f(t: number | string) { }, { code: ` +function f(t: T) { + return t; +} + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 10, + endColumn: 29, + data: { + type: 'string | number', + asserted: 'number | boolean', + }, + }, + ], + }, + { + code: ` +function f(t: T) { + return >t; +} + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 10, + endColumn: 28, + data: { + type: 'string | number', + asserted: 'Omit', + }, + }, + ], + }, + { + code: ` +declare const a: () => string | boolean; +const b = <() => string | number>a; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 11, + endColumn: 35, + data: { + type: '() => string | boolean', + asserted: '() => string | number', + }, + }, + ], + }, + { + code: ` interface Foo { bar: number; bas: string; @@ -556,5 +896,23 @@ const bar = foo; }, ], }, + { + code: ` +declare const foo: { hello: string } & { world: string }; +const bar = <{ hello: string; world: 'world' }>foo; + `, + errors: [ + { + messageId: 'unsafeTypeAssertion', + line: 3, + column: 13, + endColumn: 51, + data: { + type: '{ hello: string; } & { world: string; }', + asserted: '{ hello: string; world: "world"; }', + }, + }, + ], + }, ], }); From 08577a82704081f81025d9cf6f44c5484cd40b56 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Fri, 11 Oct 2024 21:10:41 +0300 Subject: [PATCH 09/25] remove unnecessary typescript 5.4 warning --- packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx index 1f333434f6ec..a387552e0414 100644 --- a/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx +++ b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx @@ -54,8 +54,6 @@ If your codebase has many unsafe type assertions, then it may be difficult to en 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. -This rule requires TypeScript v5.4 or above; you should not use it if your TypeScript version is lower. - ## Further Reading - More on TypeScript's [type assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) From 2ab1c8705758bda2a96243dc99ba52e2f29abdb7 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Fri, 11 Oct 2024 21:12:41 +0300 Subject: [PATCH 10/25] adjust format to new rules --- .../rules/no-unsafe-type-assertion.test.ts | 322 +++++++++--------- 1 file changed, 161 insertions(+), 161 deletions(-) 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 index c7d2d072f909..f46778fc1359 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts @@ -8,8 +8,8 @@ const rootPath = getFixturesRootDir(); const ruleTester = new RuleTester({ languageOptions: { parserOptions: { - tsconfigRootDir: rootPath, project: './tsconfig.json', + tsconfigRootDir: rootPath, }, }, }); @@ -187,14 +187,14 @@ const b = a as string; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 11, - endColumn: 22, data: { - type: 'string | number', asserted: 'string', + type: 'string | number', }, + endColumn: 22, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -205,14 +205,14 @@ const b = a as unknown as number; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 11, - endColumn: 33, data: { - type: 'unknown', asserted: 'number', + type: 'unknown', }, + endColumn: 33, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -223,14 +223,14 @@ const b = a as string | boolean; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 11, - endColumn: 32, data: { - type: 'string | undefined', asserted: 'string | boolean', + type: 'string | undefined', }, + endColumn: 32, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -241,14 +241,14 @@ const b = a as string | boolean as boolean; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 11, - endColumn: 43, data: { - type: 'string | boolean', asserted: 'boolean', + type: 'string | boolean', }, + endColumn: 43, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -259,24 +259,24 @@ const b = a as 'foo' as 'bar'; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 11, - endColumn: 30, data: { - type: '"foo"', asserted: '"bar"', + type: '"foo"', }, + endColumn: 30, + line: 3, + messageId: 'unsafeTypeAssertion', }, { - messageId: 'unsafeTypeAssertion', - line: 3, column: 11, - endColumn: 21, data: { - type: 'string', asserted: '"foo"', + type: 'string', }, + endColumn: 21, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -288,14 +288,14 @@ function f(t: number | string) { `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 10, - endColumn: 31, data: { - type: 'string | number', asserted: 'number | boolean', + type: 'string | number', }, + endColumn: 31, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -307,14 +307,14 @@ function f(t: T) { `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 10, - endColumn: 31, data: { - type: 'string | number', asserted: 'number | boolean', + type: 'string | number', }, + endColumn: 31, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -326,14 +326,14 @@ function f(t: T) { `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 10, - endColumn: 30, data: { - type: 'string | number', asserted: 'Omit', + type: 'string | number', }, + endColumn: 30, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -344,14 +344,14 @@ const b = a as () => string | number; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 11, - endColumn: 37, data: { - type: '() => string | boolean', asserted: '() => string | number', + type: '() => string | boolean', }, + endColumn: 37, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -366,14 +366,14 @@ var foo = {} as Foo; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 7, column: 11, - endColumn: 20, data: { - type: '{}', asserted: 'Foo', + type: '{}', }, + endColumn: 20, + line: 7, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -388,14 +388,14 @@ export const foo = { bar: 1, bazz: 1 } as Foo; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 7, column: 20, - endColumn: 46, data: { - type: '{ bar: number; bazz: number; }', asserted: 'Foo', + type: '{ bar: number; bazz: number; }', }, + endColumn: 46, + line: 7, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -406,24 +406,24 @@ const bar = foo as string | boolean as string | null; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 13, - endColumn: 53, data: { - type: 'string | boolean', asserted: 'string | null', + type: 'string | boolean', }, + endColumn: 53, + line: 3, + messageId: 'unsafeTypeAssertion', }, { - messageId: 'unsafeTypeAssertion', - line: 3, column: 13, - endColumn: 36, data: { - type: 'string | number', asserted: 'string | boolean', + type: 'string | number', }, + endColumn: 36, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -434,14 +434,14 @@ declare const foo: { bar?: { bazz: string } }; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 2, - endColumn: 39, data: { - type: '{ bazz: string; } | undefined', asserted: '{ bazz: string | boolean; }', + type: '{ bazz: string; } | undefined', }, + endColumn: 39, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -452,14 +452,14 @@ const bar = foo as 'hello'; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 13, - endColumn: 27, data: { - type: '"hello" | "world"', asserted: '"hello"', + type: '"hello" | "world"', }, + endColumn: 27, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -480,14 +480,14 @@ const bar = foo as Foo; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 13, column: 13, - endColumn: 23, data: { - type: 'Bazz', asserted: 'Foo', + type: 'Bazz', }, + endColumn: 23, + line: 13, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -500,14 +500,14 @@ const bar = foo as Foo; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 5, column: 13, - endColumn: 23, data: { - type: '{}', asserted: 'Readonly>', + type: '{}', }, + endColumn: 23, + line: 5, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -518,14 +518,14 @@ const bar = foo as number[]; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 13, - endColumn: 28, data: { - type: 'readonly number[]', asserted: 'number[]', + type: 'readonly number[]', }, + endColumn: 28, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -536,14 +536,14 @@ const bar = foo as { hello: string; world: 'world' }; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 13, - endColumn: 53, data: { - type: '{ hello: string; } & { world: string; }', asserted: '{ hello: string; world: "world"; }', + type: '{ hello: string; } & { world: string; }', }, + endColumn: 53, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -554,14 +554,14 @@ const b = a; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 11, - endColumn: 20, data: { - type: 'string | number', asserted: 'string', + type: 'string | number', }, + endColumn: 20, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -572,14 +572,14 @@ const b = (a); `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 11, - endColumn: 31, data: { - type: 'unknown', asserted: 'number', + type: 'unknown', }, + endColumn: 31, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -590,14 +590,14 @@ const b = a; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 11, - endColumn: 30, data: { - type: 'string | undefined', asserted: 'string | boolean', + type: 'string | undefined', }, + endColumn: 30, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -608,14 +608,14 @@ const b = (a); `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 11, - endColumn: 41, data: { - type: 'string | boolean', asserted: 'boolean', + type: 'string | boolean', }, + endColumn: 41, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -626,24 +626,24 @@ const b = <'bar'>(<'foo'>a); `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 11, - endColumn: 28, data: { - type: '"foo"', asserted: '"bar"', + type: '"foo"', }, + endColumn: 28, + line: 3, + messageId: 'unsafeTypeAssertion', }, { - messageId: 'unsafeTypeAssertion', - line: 3, column: 19, - endColumn: 27, data: { - type: 'string', asserted: '"foo"', + type: 'string', }, + endColumn: 27, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -655,14 +655,14 @@ function f(t: number | string) { `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 10, - endColumn: 29, data: { - type: 'string | number', asserted: 'number | boolean', + type: 'string | number', }, + endColumn: 29, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -674,14 +674,14 @@ function f(t: T) { `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 10, - endColumn: 29, data: { - type: 'string | number', asserted: 'number | boolean', + type: 'string | number', }, + endColumn: 29, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -693,14 +693,14 @@ function f(t: T) { `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 10, - endColumn: 28, data: { - type: 'string | number', asserted: 'Omit', + type: 'string | number', }, + endColumn: 28, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -711,14 +711,14 @@ const b = <() => string | number>a; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 11, - endColumn: 35, data: { - type: '() => string | boolean', asserted: '() => string | number', + type: '() => string | boolean', }, + endColumn: 35, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -733,14 +733,14 @@ var foo = {}; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 7, column: 11, - endColumn: 18, data: { - type: '{}', asserted: 'Foo', + type: '{}', }, + endColumn: 18, + line: 7, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -755,14 +755,14 @@ export const foo = { bar: 1, bazz: 1 }; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 7, column: 20, - endColumn: 44, data: { - type: '{ bar: number; bazz: number; }', asserted: 'Foo', + type: '{ bar: number; bazz: number; }', }, + endColumn: 44, + line: 7, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -773,24 +773,24 @@ const bar = (foo); `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 13, - endColumn: 51, data: { - type: 'string | boolean', asserted: 'string | null', + type: 'string | boolean', }, + endColumn: 51, + line: 3, + messageId: 'unsafeTypeAssertion', }, { - messageId: 'unsafeTypeAssertion', - line: 3, column: 29, - endColumn: 50, data: { - type: 'string | number', asserted: 'string | boolean', + type: 'string | number', }, + endColumn: 50, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -801,14 +801,14 @@ declare const foo: { bar?: { bazz: string } }; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 2, - endColumn: 37, data: { - type: '{ bazz: string; } | undefined', asserted: '{ bazz: string | boolean; }', + type: '{ bazz: string; } | undefined', }, + endColumn: 37, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -819,14 +819,14 @@ const bar = <'hello'>foo; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 13, - endColumn: 25, data: { - type: '"hello" | "world"', asserted: '"hello"', + type: '"hello" | "world"', }, + endColumn: 25, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -847,14 +847,14 @@ const bar = foo; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 13, column: 13, - endColumn: 21, data: { - type: 'Bazz', asserted: 'Foo', + type: 'Bazz', }, + endColumn: 21, + line: 13, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -867,14 +867,14 @@ const bar = foo; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 5, column: 13, - endColumn: 21, data: { - type: '{}', asserted: 'Readonly>', + type: '{}', }, + endColumn: 21, + line: 5, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -885,14 +885,14 @@ const bar = foo; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 13, - endColumn: 26, data: { - type: 'readonly number[]', asserted: 'number[]', + type: 'readonly number[]', }, + endColumn: 26, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, @@ -903,14 +903,14 @@ const bar = <{ hello: string; world: 'world' }>foo; `, errors: [ { - messageId: 'unsafeTypeAssertion', - line: 3, column: 13, - endColumn: 51, data: { - type: '{ hello: string; } & { world: string; }', asserted: '{ hello: string; world: "world"; }', + type: '{ hello: string; } & { world: string; }', }, + endColumn: 51, + line: 3, + messageId: 'unsafeTypeAssertion', }, ], }, From a5dbd5bfd09287af909adb26a833403146e98548 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Fri, 11 Oct 2024 21:21:46 +0300 Subject: [PATCH 11/25] update error message to be more concise --- .../src/rules/no-unsafe-type-assertion.ts | 3 +- .../rules/no-unsafe-type-assertion.test.ts | 40 ------------------- 2 files changed, 1 insertion(+), 42 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts index 5ed61ce071b3..0725d0ba93dd 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts @@ -16,7 +16,7 @@ export default createRule({ }, messages: { unsafeTypeAssertion: - 'Unsafe type assertion: type `{{type}}` is not assignable to type `{{asserted}}`', + "Unsafe type assertion: type '{{type}}' is more narrow than the original type.", }, schema: [], }, @@ -45,7 +45,6 @@ export default createRule({ messageId: 'unsafeTypeAssertion', data: { type: checker.typeToString(nodeType), - asserted: checker.typeToString(assertedType), }, }); } 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 index f46778fc1359..a1627f49e018 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts @@ -189,7 +189,6 @@ const b = a as string; { column: 11, data: { - asserted: 'string', type: 'string | number', }, endColumn: 22, @@ -207,7 +206,6 @@ const b = a as unknown as number; { column: 11, data: { - asserted: 'number', type: 'unknown', }, endColumn: 33, @@ -225,7 +223,6 @@ const b = a as string | boolean; { column: 11, data: { - asserted: 'string | boolean', type: 'string | undefined', }, endColumn: 32, @@ -243,7 +240,6 @@ const b = a as string | boolean as boolean; { column: 11, data: { - asserted: 'boolean', type: 'string | boolean', }, endColumn: 43, @@ -261,7 +257,6 @@ const b = a as 'foo' as 'bar'; { column: 11, data: { - asserted: '"bar"', type: '"foo"', }, endColumn: 30, @@ -271,7 +266,6 @@ const b = a as 'foo' as 'bar'; { column: 11, data: { - asserted: '"foo"', type: 'string', }, endColumn: 21, @@ -290,7 +284,6 @@ function f(t: number | string) { { column: 10, data: { - asserted: 'number | boolean', type: 'string | number', }, endColumn: 31, @@ -309,7 +302,6 @@ function f(t: T) { { column: 10, data: { - asserted: 'number | boolean', type: 'string | number', }, endColumn: 31, @@ -328,7 +320,6 @@ function f(t: T) { { column: 10, data: { - asserted: 'Omit', type: 'string | number', }, endColumn: 30, @@ -346,7 +337,6 @@ const b = a as () => string | number; { column: 11, data: { - asserted: '() => string | number', type: '() => string | boolean', }, endColumn: 37, @@ -368,7 +358,6 @@ var foo = {} as Foo; { column: 11, data: { - asserted: 'Foo', type: '{}', }, endColumn: 20, @@ -390,7 +379,6 @@ export const foo = { bar: 1, bazz: 1 } as Foo; { column: 20, data: { - asserted: 'Foo', type: '{ bar: number; bazz: number; }', }, endColumn: 46, @@ -408,7 +396,6 @@ const bar = foo as string | boolean as string | null; { column: 13, data: { - asserted: 'string | null', type: 'string | boolean', }, endColumn: 53, @@ -418,7 +405,6 @@ const bar = foo as string | boolean as string | null; { column: 13, data: { - asserted: 'string | boolean', type: 'string | number', }, endColumn: 36, @@ -436,7 +422,6 @@ declare const foo: { bar?: { bazz: string } }; { column: 2, data: { - asserted: '{ bazz: string | boolean; }', type: '{ bazz: string; } | undefined', }, endColumn: 39, @@ -454,7 +439,6 @@ const bar = foo as 'hello'; { column: 13, data: { - asserted: '"hello"', type: '"hello" | "world"', }, endColumn: 27, @@ -482,7 +466,6 @@ const bar = foo as Foo; { column: 13, data: { - asserted: 'Foo', type: 'Bazz', }, endColumn: 23, @@ -502,7 +485,6 @@ const bar = foo as Foo; { column: 13, data: { - asserted: 'Readonly>', type: '{}', }, endColumn: 23, @@ -520,7 +502,6 @@ const bar = foo as number[]; { column: 13, data: { - asserted: 'number[]', type: 'readonly number[]', }, endColumn: 28, @@ -538,7 +519,6 @@ const bar = foo as { hello: string; world: 'world' }; { column: 13, data: { - asserted: '{ hello: string; world: "world"; }', type: '{ hello: string; } & { world: string; }', }, endColumn: 53, @@ -556,7 +536,6 @@ const b = a; { column: 11, data: { - asserted: 'string', type: 'string | number', }, endColumn: 20, @@ -574,7 +553,6 @@ const b = (a); { column: 11, data: { - asserted: 'number', type: 'unknown', }, endColumn: 31, @@ -592,7 +570,6 @@ const b = a; { column: 11, data: { - asserted: 'string | boolean', type: 'string | undefined', }, endColumn: 30, @@ -610,7 +587,6 @@ const b = (a); { column: 11, data: { - asserted: 'boolean', type: 'string | boolean', }, endColumn: 41, @@ -628,7 +604,6 @@ const b = <'bar'>(<'foo'>a); { column: 11, data: { - asserted: '"bar"', type: '"foo"', }, endColumn: 28, @@ -638,7 +613,6 @@ const b = <'bar'>(<'foo'>a); { column: 19, data: { - asserted: '"foo"', type: 'string', }, endColumn: 27, @@ -657,7 +631,6 @@ function f(t: number | string) { { column: 10, data: { - asserted: 'number | boolean', type: 'string | number', }, endColumn: 29, @@ -676,7 +649,6 @@ function f(t: T) { { column: 10, data: { - asserted: 'number | boolean', type: 'string | number', }, endColumn: 29, @@ -695,7 +667,6 @@ function f(t: T) { { column: 10, data: { - asserted: 'Omit', type: 'string | number', }, endColumn: 28, @@ -713,7 +684,6 @@ const b = <() => string | number>a; { column: 11, data: { - asserted: '() => string | number', type: '() => string | boolean', }, endColumn: 35, @@ -735,7 +705,6 @@ var foo = {}; { column: 11, data: { - asserted: 'Foo', type: '{}', }, endColumn: 18, @@ -757,7 +726,6 @@ export const foo = { bar: 1, bazz: 1 }; { column: 20, data: { - asserted: 'Foo', type: '{ bar: number; bazz: number; }', }, endColumn: 44, @@ -775,7 +743,6 @@ const bar = (foo); { column: 13, data: { - asserted: 'string | null', type: 'string | boolean', }, endColumn: 51, @@ -785,7 +752,6 @@ const bar = (foo); { column: 29, data: { - asserted: 'string | boolean', type: 'string | number', }, endColumn: 50, @@ -803,7 +769,6 @@ declare const foo: { bar?: { bazz: string } }; { column: 2, data: { - asserted: '{ bazz: string | boolean; }', type: '{ bazz: string; } | undefined', }, endColumn: 37, @@ -821,7 +786,6 @@ const bar = <'hello'>foo; { column: 13, data: { - asserted: '"hello"', type: '"hello" | "world"', }, endColumn: 25, @@ -849,7 +813,6 @@ const bar = foo; { column: 13, data: { - asserted: 'Foo', type: 'Bazz', }, endColumn: 21, @@ -869,7 +832,6 @@ const bar = foo; { column: 13, data: { - asserted: 'Readonly>', type: '{}', }, endColumn: 21, @@ -887,7 +849,6 @@ const bar = foo; { column: 13, data: { - asserted: 'number[]', type: 'readonly number[]', }, endColumn: 26, @@ -905,7 +866,6 @@ const bar = <{ hello: string; world: 'world' }>foo; { column: 13, data: { - asserted: '{ hello: string; world: "world"; }', type: '{ hello: string; } & { world: string; }', }, endColumn: 51, From ff73c4d78f35e87f95c46264f9ea56d18469c6c2 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 15 Oct 2024 23:30:16 +0300 Subject: [PATCH 12/25] match implementation to be inline with no-unsafe-* rules --- .../src/rules/no-unsafe-type-assertion.ts | 124 +++++++++++++++++- 1 file changed, 121 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts index 0725d0ba93dd..3b09eb80df4c 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts @@ -1,9 +1,16 @@ import type { TSESTree } from '@typescript-eslint/utils'; +import * as tsutils from 'ts-api-utils'; +import * as ts from 'typescript'; + import { createRule, getConstrainedTypeAtLocation, getParserServices, + isTypeAnyType, + isTypeFlagSet, + isTypeUnknownType, + isUnsafeAssignment, } from '../util'; export default createRule({ @@ -15,6 +22,10 @@ export default createRule({ 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.", }, @@ -24,18 +35,125 @@ export default createRule({ create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); + const compilerOptions = services.program.getCompilerOptions(); + + function isTypeUnchanged(uncast: ts.Type, cast: ts.Type): boolean { + if (uncast === cast) { + return true; + } + + if ( + isTypeFlagSet(uncast, ts.TypeFlags.Undefined) && + isTypeFlagSet(cast, ts.TypeFlags.Undefined) && + tsutils.isCompilerOptionEnabled( + compilerOptions, + 'exactOptionalPropertyTypes', + ) + ) { + const uncastParts = tsutils + .unionTypeParts(uncast) + .filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); + + const castParts = tsutils + .unionTypeParts(cast) + .filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); + + if (uncastParts.length !== castParts.length) { + return false; + } + + const uncastPartsSet = new Set(uncastParts); + return castParts.every(part => uncastPartsSet.has(part)); + } + + return false; + } function checkExpression( node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, ): void { - const nodeType = getConstrainedTypeAtLocation(services, node.expression); + const expressionType = getConstrainedTypeAtLocation( + services, + node.expression, + ); const assertedType = getConstrainedTypeAtLocation( services, node.typeAnnotation, ); + // consider unchanged type as safe + const typeIsUnchanged = isTypeUnchanged(expressionType, assertedType); + + if (typeIsUnchanged) { + return; + } + + // handle cases when casting unknown ==> any. + if (isTypeAnyType(assertedType) && isTypeUnknownType(expressionType)) { + context.report({ + node, + messageId: 'unsafeToAnyTypeAssertion', + data: { + type: '`any`', + }, + }); + + return; + } + + // handle cases when casting of an any expression. + const expressionAny = isUnsafeAssignment( + expressionType, + assertedType, + checker, + node.expression, + ); + + if (expressionAny) { + context.report({ + node, + messageId: 'unsafeOfAnyTypeAssertion', + data: { + type: tsutils.isIntrinsicErrorType(expressionAny.sender) + ? 'error typed' + : '`any`', + }, + }); + + return; + } + + // handle cases when casting to an any type. + const assertedAny = isUnsafeAssignment( + assertedType, + expressionType, + checker, + node.typeAnnotation, + ); + + if (assertedAny) { + context.report({ + node, + messageId: 'unsafeToAnyTypeAssertion', + data: { + type: tsutils.isIntrinsicErrorType(assertedAny.sender) + ? 'error typed' + : '`any`', + }, + }); + + return; + } + + // fallback to checking assignability + const nodeWidenedType = + tsutils.isObjectType(expressionType) && + tsutils.isObjectFlagSet(expressionType, ts.ObjectFlags.ObjectLiteral) + ? checker.getWidenedType(expressionType) + : expressionType; + const isAssertionSafe = checker.isTypeAssignableTo( - nodeType, + nodeWidenedType, assertedType, ); @@ -44,7 +162,7 @@ export default createRule({ node, messageId: 'unsafeTypeAssertion', data: { - type: checker.typeToString(nodeType), + type: checker.typeToString(expressionType), }, }); } From 913529f162e7a21e27ea77677180e285b047386b Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 15 Oct 2024 23:30:29 +0300 Subject: [PATCH 13/25] rework tests --- .../rules/no-unsafe-type-assertion.test.ts | 1830 +++++++++-------- 1 file changed, 999 insertions(+), 831 deletions(-) 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 index a1627f49e018..cc6e8ab4c53d 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts @@ -14,865 +14,1033 @@ const ruleTester = new RuleTester({ }, }); -ruleTester.run('no-unsafe-type-assertion', rule, { - valid: [ - ` +describe('basic assertions', () => { + ruleTester.run('no-unsafe-type-assertion', rule, { + valid: [ + ` declare const a: string; -const b = a as string | number; - `, - ` -declare const a: string; -const b = a as unknown; - `, - ` -declare const a: string; -const b = a as any; - `, - ` +a as string | number; + `, + ` declare const a: string; -const b = a as string | number as string | number | boolean; - `, - ` +a; + `, + ` declare const a: string; -const b = a as any as number; - `, - ` -declare const a: () => boolean; -const b = a() as boolean | number; - `, - ` -declare const a: () => boolean; -const b = a() as boolean | number as boolean | number | string; - `, - ` +a as string | number as string | number | boolean; + `, + ` declare const a: string; -const b = a as string; - `, - ` -declare const a: () => string; -const b = a() as string; - `, - ` -declare const a: () => string; -const b = a as (() => string) | (() => number); - `, - ` -declare const a: () => string; -const b = a as (() => string) | ((x: number) => string); - `, - ` -declare const a: () => string; -const b = a as () => string | number; - `, - ` +a as string; + `, + ` declare const a: { hello: 'world' }; -const b = a as { hello: string }; - `, - ` -declare const foo = 'hello' as const; -foo() as string; - `, - ` -declare const foo: () => string | undefined; -foo()!; - `, - ` -declare const foo: { bar?: { bazz: string } }; -(foo.bar as { bazz: string | boolean } | undefined)?.bazz; - `, - ` -function foo(a: string) { - return a as string | number; -} - `, - ` -function foo(a: T) { - return a as boolean | number; -} - `, - ` +a as { hello: string }; + `, + ` +'hello' as const; + `, + ` function foo(a: T) { return a as T | number; } - `, - ` -declare const a: { hello: string } & { world: string }; -const b = a as { hello: string }; - `, - ` -declare const a: string; -const b = a; - `, - ` -declare const a: string; -const b = a; - `, - ` -declare const a: string; -const b = a; - `, - ` -declare const a: string; -const b = (a); - `, - ` -declare const a: string; -const b = (a); - `, - ` -declare const a: () => boolean; -const b = a(); - `, - ` -declare const a: () => boolean; -const b = boolean | number | string(a()); - `, - ` -declare const a: string; -const b = a; - `, - ` -declare const a: () => string; -const b = a(); - `, - ` -declare const a: () => string; -const b = <(() => string) | (() => number)>a; - `, - ` -declare const a: () => string; -const b = <(() => string) | ((x: number) => string)>a; - `, - ` -declare const a: () => string; -const b = <() => string | number>a; - `, - ` -declare const a: { hello: 'world' }; -const b = <{ hello: string }>a; - `, - ` -declare const foo = 'hello'; -foo(); - `, - ` -declare const foo: { bar?: { bazz: string } }; -(<{ bazz: string | boolean } | undefined>foo.bar)?.bazz; - `, - ` -function foo(a: string) { - return a; -} - `, - ` -function foo(a: T) { - return a; -} - `, - ` -function foo(a: T) { - return a; -} - `, - ` -declare const a: { hello: string } & { world: string }; -const b = <{ hello: string }>a; - `, - ], - invalid: [ - { - code: ` + `, + ], + invalid: [ + { + code: ` declare const a: string | number; -const b = a as string; - `, - errors: [ - { - column: 11, - data: { - type: 'string | number', - }, - endColumn: 22, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const a: string; -const b = a as unknown as number; - `, - errors: [ - { - column: 11, - data: { - type: 'unknown', - }, - endColumn: 33, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` +a as string; + `, + errors: [ + { + column: 1, + data: { + type: 'string | number', + }, + 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 | number', + }, + endColumn: 29, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: string | number; +a; + `, + errors: [ + { + column: 1, + data: { + type: 'string | number', + }, + endColumn: 10, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` declare const a: string | undefined; -const b = a as string | boolean; - `, - errors: [ - { - column: 11, - data: { - type: 'string | undefined', - }, - endColumn: 32, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const a: string; -const b = a as string | boolean as boolean; - `, - errors: [ - { - column: 11, - data: { - type: 'string | boolean', - }, - endColumn: 43, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` +a as string | boolean; + `, + errors: [ + { + column: 1, + data: { + type: 'string | undefined', + }, + endColumn: 22, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + // multiple failures + { + code: ` declare const a: string; -const b = a as 'foo' as 'bar'; - `, - errors: [ - { - column: 11, - data: { - type: '"foo"', - }, - endColumn: 30, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - { - column: 11, - data: { - type: 'string', - }, - endColumn: 21, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -function f(t: number | string) { - return t as number | boolean; +a as 'foo' as 'bar'; + `, + errors: [ + { + column: 1, + data: { + type: '"foo"', + }, + endColumn: 20, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + { + column: 1, + data: { + type: 'string', + }, + endColumn: 11, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + // type constraint + { + code: ` +function foo(a: T) { + return a as true; } + `, + errors: [ + { + column: 10, + data: { + type: 'boolean', + }, + endColumn: 19, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + // long/complex original type + { + code: ` +declare const a: Omit< + Required>, + 'foo' +>; +a as string; + `, + errors: [ + { + column: 1, + data: { + type: 'Omit>, "foo">', + }, + endColumn: 12, + endLine: 6, + line: 6, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const foo: readonly number[]; +const bar = foo as number[]; + `, + errors: [ + { + column: 13, + data: { + type: 'readonly 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; `, - errors: [ - { - column: 10, - data: { - type: 'string | number', - }, - endColumn: 31, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -function f(t: T) { - return t as number | boolean; -} + ` +declare const _any_: any; +_any_ as unknown; `, - errors: [ - { - column: 10, - data: { - type: 'string | number', - }, - endColumn: 31, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -function f(t: T) { - return t as Omit; -} + ], + 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; `, - errors: [ - { - column: 10, - data: { - type: 'string | number', - }, - endColumn: 30, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const a: () => string | boolean; -const b = a as () => string | number; - `, - errors: [ - { - column: 11, - data: { - type: '() => string | boolean', - }, - endColumn: 37, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -interface Foo { - bar: number; - bas: string; -} + ` +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', + }, + ], + }, + ], + }); +}); -var foo = {} as Foo; - `, - errors: [ - { - column: 11, - data: { - type: '{}', - }, - endColumn: 20, - line: 7, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -interface Foo { - bar: number; -} +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', + }, + ], + }, + ], + }); +}); -// no additional properties are allowed -export const foo = { bar: 1, bazz: 1 } as Foo; - `, - errors: [ - { - column: 20, - data: { - type: '{ bar: number; bazz: number; }', - }, - endColumn: 46, - line: 7, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const foo: string | number; -const bar = foo as string | boolean as string | null; - `, - errors: [ - { - column: 13, - data: { - type: 'string | boolean', - }, - endColumn: 53, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - { - column: 13, - data: { - type: 'string | number', - }, - endColumn: 36, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const foo: { bar?: { bazz: string } }; -(foo.bar as { bazz: string | boolean }).bazz; - `, - errors: [ - { - column: 2, - data: { - type: '{ bazz: string; } | undefined', - }, - endColumn: 39, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const foo: 'hello' | 'world'; -const bar = foo as 'hello'; - `, - errors: [ - { - column: 13, - data: { - type: '"hello" | "world"', - }, - endColumn: 27, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -interface Foo { - type: 'foo'; -} +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: '{}', + }, + 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; }', + }, + 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', + }, + ], + }, + ], + }); +}); -interface Bar { - type: 'bar'; -} +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 | number)[]', + }, + 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', + }, + ], + }, + ], + }); +}); -type Bazz = Foo | Bar; +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 | number]', + }, + endColumn: 14, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: [string, number]; +a as [string, string]; + `, + errors: [ + { + column: 1, + data: { + type: '[string, number]', + }, + endColumn: 22, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: [string]; +a as [string, number]; + `, + errors: [ + { + column: 1, + data: { + type: '[string]', + }, + endColumn: 22, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +declare const a: [string, number]; +a as [string]; + `, + errors: [ + { + column: 1, + data: { + type: '[string, number]', + }, + 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', + }, + ], + }, + ], + }); +}); -declare const foo: Bazz; -const bar = foo as Foo; - `, - errors: [ - { - column: 13, - data: { - type: 'Bazz', - }, - endColumn: 23, - line: 13, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -type Foo = Readonly>; +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', + }, + ], + }, + ], + }); +}); -declare const foo: {}; -const bar = foo as Foo; - `, - errors: [ - { - column: 13, - data: { - type: '{}', - }, - endColumn: 23, - line: 5, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const foo: readonly number[]; -const bar = foo as number[]; +describe('class assertions', () => { + ruleTester.run('no-unsafe-type-assertion', rule, { + valid: [ + ` +class Foo {} +declare const a: Foo; +a as Foo | number; `, - errors: [ - { - column: 13, - data: { - type: 'readonly number[]', - }, - endColumn: 28, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const foo: { hello: string } & { world: string }; -const bar = foo as { hello: string; world: 'world' }; - `, - errors: [ - { - column: 13, - data: { - type: '{ hello: string; } & { world: string; }', - }, - endColumn: 53, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const a: string | number; -const b = a; - `, - errors: [ - { - column: 11, - data: { - type: 'string | number', - }, - endColumn: 20, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const a: string; -const b = (a); - `, - errors: [ - { - column: 11, - data: { - type: 'unknown', - }, - endColumn: 31, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const a: string | undefined; -const b = a; - `, - errors: [ - { - column: 11, - data: { - type: 'string | undefined', - }, - endColumn: 30, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const a: string; -const b = (a); - `, - errors: [ - { - column: 11, - data: { - type: 'string | boolean', - }, - endColumn: 41, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const a: string; -const b = <'bar'>(<'foo'>a); - `, - errors: [ - { - column: 11, - data: { - type: '"foo"', - }, - endColumn: 28, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - { - column: 19, - data: { - type: 'string', - }, - endColumn: 27, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -function f(t: number | string) { - return t; -} + ` +class Foo {} +class Bar {} +declare const a: Foo; +a as Bar; `, - errors: [ - { - column: 10, - data: { - type: 'string | number', - }, - endColumn: 29, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -function f(t: T) { - return t; + ` +class Foo { + hello() {} } +class Bar {} +declare const a: Foo; +a as Bar; `, - errors: [ - { - column: 10, - data: { - type: 'string | number', - }, - endColumn: 29, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -function f(t: T) { - return >t; + ` +class Foo { + hello() {} } +class Bar extends Foo {} +declare const a: Bar; +a as Foo; `, - errors: [ - { - column: 10, - data: { - type: 'string | number', - }, - endColumn: 28, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const a: () => string | boolean; -const b = <() => string | number>a; - `, - errors: [ - { - column: 11, - data: { - type: '() => string | boolean', - }, - endColumn: 35, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -interface Foo { - bar: number; - bas: string; -} - -var foo = {}; - `, - errors: [ - { - column: 11, - data: { - type: '{}', - }, - endColumn: 18, - line: 7, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -interface Foo { - bar: number; + ` +class Foo { + hello() {} } - -// no additional properties are allowed -export const foo = { bar: 1, bazz: 1 }; - `, - errors: [ - { - column: 20, - data: { - type: '{ bar: number; bazz: number; }', - }, - endColumn: 44, - line: 7, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const foo: string | number; -const bar = (foo); - `, - errors: [ - { - column: 13, - data: { - type: 'string | boolean', - }, - endColumn: 51, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - { - column: 29, - data: { - type: 'string | number', - }, - endColumn: 50, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const foo: { bar?: { bazz: string } }; -(<{ bazz: string | boolean }>foo.bar).bazz; - `, - errors: [ - { - column: 2, - data: { - type: '{ bazz: string; } | undefined', - }, - endColumn: 37, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const foo: 'hello' | 'world'; -const bar = <'hello'>foo; - `, - errors: [ - { - column: 13, - data: { - type: '"hello" | "world"', - }, - endColumn: 25, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -interface Foo { - type: 'foo'; +class Bar extends Foo {} +declare const a: Foo; +a as Bar; + `, + ], + invalid: [ + { + code: ` +class Foo { + hello() {} } - -interface Bar { - type: 'bar'; +class Bar extends Foo { + world() {} } - -type Bazz = Foo | Bar; - -declare const foo: Bazz; -const bar = foo; - `, - errors: [ - { - column: 13, - data: { - type: 'Bazz', - }, - endColumn: 21, - line: 13, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -type Foo = Readonly>; - -declare const foo: {}; -const bar = foo; - `, - errors: [ - { - column: 13, - data: { - type: '{}', - }, - endColumn: 21, - line: 5, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const foo: readonly number[]; -const bar = foo; - `, - errors: [ - { - column: 13, - data: { - type: 'readonly number[]', - }, - endColumn: 26, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - { - code: ` -declare const foo: { hello: string } & { world: string }; -const bar = <{ hello: string; world: 'world' }>foo; - `, - errors: [ - { - column: 13, - data: { - type: '{ hello: string; } & { world: string; }', - }, - endColumn: 51, - line: 3, - messageId: 'unsafeTypeAssertion', - }, - ], - }, - ], +declare const a: Foo; +a as Bar; + `, + errors: [ + { + column: 1, + endColumn: 9, + endLine: 9, + line: 9, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + ], + }); }); From 88d15deb99d38c65ec6021ccb681c9f4ac5e52f6 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 15 Oct 2024 23:31:40 +0300 Subject: [PATCH 14/25] refactor --- .../rules/no-unnecessary-type-assertion.ts | 39 ++---------- .../src/rules/no-unsafe-type-assertion.ts | 61 ++++++------------- .../eslint-plugin/src/util/isTypeUnchanged.ts | 39 ++++++++++++ 3 files changed, 64 insertions(+), 75 deletions(-) create mode 100644 packages/eslint-plugin/src/util/isTypeUnchanged.ts diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts index b2c4697d74f8..c38b4a46a771 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts @@ -18,6 +18,7 @@ import { nullThrows, NullThrowsReasons, } from '../util'; +import { isTypeUnchanged } from '../util/isTypeUnchanged'; type Options = [ { @@ -176,38 +177,6 @@ export default createRule({ ); } - function isTypeUnchanged(uncast: ts.Type, cast: ts.Type): boolean { - if (uncast === cast) { - return true; - } - - if ( - isTypeFlagSet(uncast, ts.TypeFlags.Undefined) && - isTypeFlagSet(cast, ts.TypeFlags.Undefined) && - tsutils.isCompilerOptionEnabled( - compilerOptions, - 'exactOptionalPropertyTypes', - ) - ) { - const uncastParts = tsutils - .unionTypeParts(uncast) - .filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); - - const castParts = tsutils - .unionTypeParts(cast) - .filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); - - if (uncastParts.length !== castParts.length) { - return false; - } - - const uncastPartsSet = new Set(uncastParts); - return castParts.every(part => uncastPartsSet.has(part)); - } - - return false; - } - return { 'TSAsExpression, TSTypeAssertion'( node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, @@ -222,7 +191,11 @@ export default createRule({ const castType = services.getTypeAtLocation(node); const uncastType = services.getTypeAtLocation(node.expression); - const typeIsUnchanged = isTypeUnchanged(uncastType, castType); + const typeIsUnchanged = isTypeUnchanged( + compilerOptions, + uncastType, + castType, + ); const wouldSameTypeBeInferred = castType.isLiteral() ? isImplicitlyNarrowedConstDeclaration(node) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts index 3b09eb80df4c..47afcf667898 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts @@ -8,10 +8,10 @@ import { getConstrainedTypeAtLocation, getParserServices, isTypeAnyType, - isTypeFlagSet, isTypeUnknownType, isUnsafeAssignment, } from '../util'; +import { isTypeUnchanged } from '../util/isTypeUnchanged'; export default createRule({ name: 'no-unsafe-type-assertion', @@ -37,36 +37,15 @@ export default createRule({ const checker = services.program.getTypeChecker(); const compilerOptions = services.program.getCompilerOptions(); - function isTypeUnchanged(uncast: ts.Type, cast: ts.Type): boolean { - if (uncast === cast) { - return true; - } - - if ( - isTypeFlagSet(uncast, ts.TypeFlags.Undefined) && - isTypeFlagSet(cast, ts.TypeFlags.Undefined) && - tsutils.isCompilerOptionEnabled( - compilerOptions, - 'exactOptionalPropertyTypes', - ) - ) { - const uncastParts = tsutils - .unionTypeParts(uncast) - .filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); - - const castParts = tsutils - .unionTypeParts(cast) - .filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); - - if (uncastParts.length !== castParts.length) { - return false; - } - - const uncastPartsSet = new Set(uncastParts); - return castParts.every(part => uncastPartsSet.has(part)); - } + function getAnyTypeName(type: ts.Type): string { + return tsutils.isIntrinsicErrorType(type) ? 'error typed' : '`any`'; + } - return false; + function isObjectLiteralType(type: ts.Type): boolean { + return ( + tsutils.isObjectType(type) && + tsutils.isObjectFlagSet(type, ts.ObjectFlags.ObjectLiteral) + ); } function checkExpression( @@ -82,7 +61,11 @@ export default createRule({ ); // consider unchanged type as safe - const typeIsUnchanged = isTypeUnchanged(expressionType, assertedType); + const typeIsUnchanged = isTypeUnchanged( + compilerOptions, + expressionType, + assertedType, + ); if (typeIsUnchanged) { return; @@ -114,9 +97,7 @@ export default createRule({ node, messageId: 'unsafeOfAnyTypeAssertion', data: { - type: tsutils.isIntrinsicErrorType(expressionAny.sender) - ? 'error typed' - : '`any`', + type: getAnyTypeName(expressionAny.sender), }, }); @@ -136,9 +117,7 @@ export default createRule({ node, messageId: 'unsafeToAnyTypeAssertion', data: { - type: tsutils.isIntrinsicErrorType(assertedAny.sender) - ? 'error typed' - : '`any`', + type: getAnyTypeName(assertedAny.sender), }, }); @@ -146,11 +125,9 @@ export default createRule({ } // fallback to checking assignability - const nodeWidenedType = - tsutils.isObjectType(expressionType) && - tsutils.isObjectFlagSet(expressionType, ts.ObjectFlags.ObjectLiteral) - ? checker.getWidenedType(expressionType) - : expressionType; + const nodeWidenedType = isObjectLiteralType(expressionType) + ? checker.getWidenedType(expressionType) + : expressionType; const isAssertionSafe = checker.isTypeAssignableTo( nodeWidenedType, diff --git a/packages/eslint-plugin/src/util/isTypeUnchanged.ts b/packages/eslint-plugin/src/util/isTypeUnchanged.ts new file mode 100644 index 000000000000..d39f02929ac8 --- /dev/null +++ b/packages/eslint-plugin/src/util/isTypeUnchanged.ts @@ -0,0 +1,39 @@ +import { isTypeFlagSet } from '@typescript-eslint/type-utils'; +import * as tsutils from 'ts-api-utils'; +import * as ts from 'typescript'; + +export function isTypeUnchanged( + compilerOptions: ts.CompilerOptions, + uncast: ts.Type, + cast: ts.Type, +): boolean { + if (uncast === cast) { + return true; + } + + if ( + isTypeFlagSet(uncast, ts.TypeFlags.Undefined) && + isTypeFlagSet(cast, ts.TypeFlags.Undefined) && + tsutils.isCompilerOptionEnabled( + compilerOptions, + 'exactOptionalPropertyTypes', + ) + ) { + const uncastParts = tsutils + .unionTypeParts(uncast) + .filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); + + const castParts = tsutils + .unionTypeParts(cast) + .filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); + + if (uncastParts.length !== castParts.length) { + return false; + } + + const uncastPartsSet = new Set(uncastParts); + return castParts.every(part => uncastPartsSet.has(part)); + } + + return false; +} From c4f7ce757cb8e7f73bc491f99d095f86985b3b1c Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 15 Oct 2024 23:59:06 +0300 Subject: [PATCH 15/25] update snapshots --- .../no-unsafe-type-assertion.shot | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 61e77619c588..a9e4de40a2b0 100644 --- 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 @@ -8,12 +8,12 @@ function f() { } const z = f() as number; - ~~~~~~~~~~~~~ Unsafe type assertion: type \`42 | "oops"\` is not assignable to type \`number\` + ~~~~~~~~~~~~~ Unsafe type assertion: type '42 | "oops"' is more narrow than the original type. const items = [1, '2', 3, '4']; const number = items[0] as number; - ~~~~~~~~~~~~~~~~~~ Unsafe type assertion: type \`string | number\` is not assignable to type \`number\` + ~~~~~~~~~~~~~~~~~~ Unsafe type assertion: type 'string | number' is more narrow than the original type. " `; From c34f7c156f1f4c82e5233b4d6daed28b7b54dedc Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Fri, 25 Oct 2024 09:35:28 +0300 Subject: [PATCH 16/25] fix error message showing original type instead of asserted type --- .../src/rules/no-unsafe-type-assertion.ts | 2 +- .../rules/no-unsafe-type-assertion.test.ts | 42 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts index 47afcf667898..adbdd1464752 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts @@ -139,7 +139,7 @@ export default createRule({ node, messageId: 'unsafeTypeAssertion', data: { - type: checker.typeToString(expressionType), + type: checker.typeToString(assertedType), }, }); } 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 index cc6e8ab4c53d..4be1b2979af1 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts @@ -56,7 +56,7 @@ a as string; { column: 1, data: { - type: 'string | number', + type: 'string', }, endColumn: 12, endLine: 3, @@ -74,7 +74,7 @@ a satisfies string as string; { column: 1, data: { - type: 'string | number', + type: 'string', }, endColumn: 29, endLine: 3, @@ -92,7 +92,7 @@ declare const a: string | number; { column: 1, data: { - type: 'string | number', + type: 'string', }, endColumn: 10, endLine: 3, @@ -110,7 +110,7 @@ a as string | boolean; { column: 1, data: { - type: 'string | undefined', + type: 'string | boolean', }, endColumn: 22, endLine: 3, @@ -129,7 +129,7 @@ a as 'foo' as 'bar'; { column: 1, data: { - type: '"foo"', + type: '"bar"', }, endColumn: 20, endLine: 3, @@ -139,7 +139,7 @@ a as 'foo' as 'bar'; { column: 1, data: { - type: 'string', + type: '"foo"', }, endColumn: 11, endLine: 3, @@ -159,7 +159,7 @@ function foo(a: T) { { column: 10, data: { - type: 'boolean', + type: 'true', }, endColumn: 19, endLine: 3, @@ -168,14 +168,14 @@ function foo(a: T) { }, ], }, - // long/complex original type + // long/complex asserted type { code: ` -declare const a: Omit< +declare const a: string; +a as Omit< Required>, 'foo' >; -a as string; `, errors: [ { @@ -183,9 +183,9 @@ a as string; data: { type: 'Omit>, "foo">', }, - endColumn: 12, + endColumn: 2, endLine: 6, - line: 6, + line: 3, messageId: 'unsafeTypeAssertion', }, ], @@ -199,7 +199,7 @@ const bar = foo as number[]; { column: 13, data: { - type: 'readonly number[]', + type: 'number[]', }, endColumn: 28, endLine: 3, @@ -506,7 +506,7 @@ var foo = {} as { { column: 11, data: { - type: '{}', + type: '{ bar: number; bas: string; }', }, endColumn: 2, endLine: 5, @@ -524,7 +524,7 @@ a satisfies Record as { hello: string; world: string }; { column: 1, data: { - type: '{ hello: string; }', + type: '{ hello: string; world: string; }', }, endColumn: 71, endLine: 3, @@ -578,7 +578,7 @@ a as string[]; { column: 1, data: { - type: '(string | number)[]', + type: 'string[]', }, endColumn: 14, endLine: 3, @@ -687,7 +687,7 @@ a as [string]; { column: 1, data: { - type: '[string | number]', + type: '[string]', }, endColumn: 14, endLine: 3, @@ -705,7 +705,7 @@ a as [string, string]; { column: 1, data: { - type: '[string, number]', + type: '[string, string]', }, endColumn: 22, endLine: 3, @@ -723,7 +723,7 @@ a as [string, number]; { column: 1, data: { - type: '[string]', + type: '[string, number]', }, endColumn: 22, endLine: 3, @@ -741,7 +741,7 @@ a as [string]; { column: 1, data: { - type: '[string, number]', + type: '[string]', }, endColumn: 14, endLine: 3, @@ -883,7 +883,7 @@ a as Promise; { column: 1, data: { - type: 'Promise', + type: 'Promise', }, endColumn: 21, endLine: 3, From e93cca524e7d7ea6df8fb2329ae0f2b6600db489 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Fri, 25 Oct 2024 09:58:40 +0300 Subject: [PATCH 17/25] update snapshots --- .../no-unsafe-type-assertion.shot | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index a9e4de40a2b0..bcc2b982cc4d 100644 --- 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 @@ -8,12 +8,12 @@ function f() { } const z = f() as number; - ~~~~~~~~~~~~~ Unsafe type assertion: type '42 | "oops"' is more narrow than the original type. + ~~~~~~~~~~~~~ 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 'string | number' is more narrow than the original type. + ~~~~~~~~~~~~~~~~~~ Unsafe type assertion: type 'number' is more narrow than the original type. " `; From 1d322809044d024160a89f07ae459ab0cfc0f7d2 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Fri, 25 Oct 2024 10:22:59 +0300 Subject: [PATCH 18/25] add a warning for object stubbing on test files --- packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx index a387552e0414..3187c4e7f920 100644 --- a/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx +++ b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx @@ -54,6 +54,8 @@ If your codebase has many unsafe type assertions, then it may be difficult to en 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) From d4c5236a62aec0bdb8acd2fbfec951f619ea4af0 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Fri, 25 Oct 2024 10:26:07 +0300 Subject: [PATCH 19/25] fix linting --- .../tests/rules/no-unsafe-type-assertion.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 index 4be1b2979af1..bce0b57bcd07 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts @@ -172,10 +172,7 @@ function foo(a: T) { { code: ` declare const a: string; -a as Omit< - Required>, - 'foo' ->; +a as Omit>, 'foo'>; `, errors: [ { From 03ae3bc09a478df3849b6c9bc51f6721de8d0c67 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Fri, 25 Oct 2024 10:37:53 +0300 Subject: [PATCH 20/25] adjust test to lint fixes --- .../tests/rules/no-unsafe-type-assertion.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index bce0b57bcd07..aa73bc2dae4d 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts @@ -180,8 +180,8 @@ a as Omit>, 'foo'>; data: { type: 'Omit>, "foo">', }, - endColumn: 2, - endLine: 6, + endColumn: 69, + endLine: 3, line: 3, messageId: 'unsafeTypeAssertion', }, From 9980c9ecaa934f9a4829f60d84f04508375919ac Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sun, 10 Nov 2024 22:54:06 +0200 Subject: [PATCH 21/25] simplify type comparison --- .../rules/no-unnecessary-type-assertion.ts | 39 ++++++++++++++++--- .../src/rules/no-unsafe-type-assertion.ts | 10 +---- .../eslint-plugin/src/util/isTypeUnchanged.ts | 39 ------------------- 3 files changed, 34 insertions(+), 54 deletions(-) delete mode 100644 packages/eslint-plugin/src/util/isTypeUnchanged.ts diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts index c38b4a46a771..b2c4697d74f8 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts @@ -18,7 +18,6 @@ import { nullThrows, NullThrowsReasons, } from '../util'; -import { isTypeUnchanged } from '../util/isTypeUnchanged'; type Options = [ { @@ -177,6 +176,38 @@ export default createRule({ ); } + function isTypeUnchanged(uncast: ts.Type, cast: ts.Type): boolean { + if (uncast === cast) { + return true; + } + + if ( + isTypeFlagSet(uncast, ts.TypeFlags.Undefined) && + isTypeFlagSet(cast, ts.TypeFlags.Undefined) && + tsutils.isCompilerOptionEnabled( + compilerOptions, + 'exactOptionalPropertyTypes', + ) + ) { + const uncastParts = tsutils + .unionTypeParts(uncast) + .filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); + + const castParts = tsutils + .unionTypeParts(cast) + .filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); + + if (uncastParts.length !== castParts.length) { + return false; + } + + const uncastPartsSet = new Set(uncastParts); + return castParts.every(part => uncastPartsSet.has(part)); + } + + return false; + } + return { 'TSAsExpression, TSTypeAssertion'( node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, @@ -191,11 +222,7 @@ export default createRule({ const castType = services.getTypeAtLocation(node); const uncastType = services.getTypeAtLocation(node.expression); - const typeIsUnchanged = isTypeUnchanged( - compilerOptions, - uncastType, - castType, - ); + const typeIsUnchanged = isTypeUnchanged(uncastType, castType); const wouldSameTypeBeInferred = castType.isLiteral() ? isImplicitlyNarrowedConstDeclaration(node) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts index adbdd1464752..5b4eb22cda13 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts @@ -11,7 +11,6 @@ import { isTypeUnknownType, isUnsafeAssignment, } from '../util'; -import { isTypeUnchanged } from '../util/isTypeUnchanged'; export default createRule({ name: 'no-unsafe-type-assertion', @@ -35,7 +34,6 @@ export default createRule({ create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); - const compilerOptions = services.program.getCompilerOptions(); function getAnyTypeName(type: ts.Type): string { return tsutils.isIntrinsicErrorType(type) ? 'error typed' : '`any`'; @@ -61,13 +59,7 @@ export default createRule({ ); // consider unchanged type as safe - const typeIsUnchanged = isTypeUnchanged( - compilerOptions, - expressionType, - assertedType, - ); - - if (typeIsUnchanged) { + if (expressionType === assertedType) { return; } diff --git a/packages/eslint-plugin/src/util/isTypeUnchanged.ts b/packages/eslint-plugin/src/util/isTypeUnchanged.ts deleted file mode 100644 index d39f02929ac8..000000000000 --- a/packages/eslint-plugin/src/util/isTypeUnchanged.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { isTypeFlagSet } from '@typescript-eslint/type-utils'; -import * as tsutils from 'ts-api-utils'; -import * as ts from 'typescript'; - -export function isTypeUnchanged( - compilerOptions: ts.CompilerOptions, - uncast: ts.Type, - cast: ts.Type, -): boolean { - if (uncast === cast) { - return true; - } - - if ( - isTypeFlagSet(uncast, ts.TypeFlags.Undefined) && - isTypeFlagSet(cast, ts.TypeFlags.Undefined) && - tsutils.isCompilerOptionEnabled( - compilerOptions, - 'exactOptionalPropertyTypes', - ) - ) { - const uncastParts = tsutils - .unionTypeParts(uncast) - .filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); - - const castParts = tsutils - .unionTypeParts(cast) - .filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); - - if (uncastParts.length !== castParts.length) { - return false; - } - - const uncastPartsSet = new Set(uncastParts); - return castParts.every(part => uncastPartsSet.has(part)); - } - - return false; -} From 8b0912fdec3b98e919d3d6ec47a17791c649f7c8 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sun, 10 Nov 2024 23:00:00 +0200 Subject: [PATCH 22/25] rework code-comments and rename variables --- .../src/rules/no-unsafe-type-assertion.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts index 5b4eb22cda13..68f9e23501fc 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts @@ -58,7 +58,6 @@ export default createRule({ node.typeAnnotation, ); - // consider unchanged type as safe if (expressionType === assertedType) { return; } @@ -76,47 +75,46 @@ export default createRule({ return; } - // handle cases when casting of an any expression. - const expressionAny = isUnsafeAssignment( + const unsafeExpressionAny = isUnsafeAssignment( expressionType, assertedType, checker, node.expression, ); - if (expressionAny) { + if (unsafeExpressionAny) { context.report({ node, messageId: 'unsafeOfAnyTypeAssertion', data: { - type: getAnyTypeName(expressionAny.sender), + type: getAnyTypeName(unsafeExpressionAny.sender), }, }); return; } - // handle cases when casting to an any type. - const assertedAny = isUnsafeAssignment( + const unsafeAssertedAny = isUnsafeAssignment( assertedType, expressionType, checker, node.typeAnnotation, ); - if (assertedAny) { + if (unsafeAssertedAny) { context.report({ node, messageId: 'unsafeToAnyTypeAssertion', data: { - type: getAnyTypeName(assertedAny.sender), + type: getAnyTypeName(unsafeAssertedAny.sender), }, }); return; } - // fallback to checking assignability + // 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; From 07ae890b4c1eea7c1ecea53376ad035d9ce5f3bc Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sun, 10 Nov 2024 23:28:15 +0200 Subject: [PATCH 23/25] rework the opening paragraph to make it more beginner-friendly --- .../eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx index 3187c4e7f920..5ff3d397d619 100644 --- a/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx +++ b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx @@ -9,9 +9,11 @@ import TabItem from '@theme/TabItem'; > > See **https://typescript-eslint.io/rules/no-unsafe-type-assertion** for documentation. -This rule forbids using type assertions to narrow a type, as it can lead to unsafe assumptions that bypass TypeScript’s type-checking. +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. -Instead, it's better to rely on type guards to ensure type safety and avoid potential runtime errors caused by unsafe type assertions. +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 From 5d8c521972afab9048a0e4153271c576da72129f Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Mon, 11 Nov 2024 23:41:08 +0200 Subject: [PATCH 24/25] Update packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx Co-authored-by: Kirk Waiblinger --- packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx index 5ff3d397d619..53c1e51ee970 100644 --- a/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx +++ b/packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx @@ -1,5 +1,5 @@ --- -description: 'Disallow type assertions that widen a type.' +description: 'Disallow type assertions that narrow a type.' --- import Tabs from '@theme/Tabs'; From 033fd0b180703f98ff139bd5533e386bf68290dc Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 14 Nov 2024 12:19:49 -0500 Subject: [PATCH 25/25] fix: narrow/widen in description --- packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts index 68f9e23501fc..06c3dd44615a 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts @@ -17,7 +17,7 @@ export default createRule({ meta: { type: 'problem', docs: { - description: 'Disallow type assertions that widen a type', + description: 'Disallow type assertions that narrow a type', requiresTypeChecking: true, }, messages: {