diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 4cf27e64a83c..52776b44c729 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -148,6 +148,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallows returning any from a function | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | | +| [`@typescript-eslint/non-nullable-type-assertion-style`](./docs/rules/non-nullable-type-assertion-style.md) | Prefers a non-null assertion over explicit type cast when possible | | :wrench: | :thought_balloon: | | [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/prefer-enum-initializers`](./docs/rules/prefer-enum-initializers.md) | Prefer initializing each enums member value | | | | | [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Prefer a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated | | | | diff --git a/packages/eslint-plugin/docs/rules/non-nullable-type-assertion-style.md b/packages/eslint-plugin/docs/rules/non-nullable-type-assertion-style.md new file mode 100644 index 000000000000..473f83b0a9bf --- /dev/null +++ b/packages/eslint-plugin/docs/rules/non-nullable-type-assertion-style.md @@ -0,0 +1,27 @@ +# Prefers a non-null assertion over explicit type cast when possible (`non-nullable-type-assertion-style`) + +This rule detects when an `as` cast is doing the same job as a `!` would, and suggests fixing the code to be an `!`. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```ts +const maybe = Math.random() > 0.5 ? '' : undefined; + +const definitely = maybe as string; +const alsoDefinitely = maybe; +``` + +Examples of **correct** code for this rule: + +```ts +const maybe = Math.random() > 0.5 ? '' : undefined; + +const definitely = maybe!; +const alsoDefinitely = maybe!; +``` + +## When Not To Use It + +If you don't mind having unnecessarily verbose type casts, you can avoid this rule. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 07234c9950f5..df0b704146ce 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -112,6 +112,7 @@ export = { 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', '@typescript-eslint/no-var-requires': 'error', + '@typescript-eslint/non-nullable-type-assertion-style': 'error', '@typescript-eslint/prefer-as-const': 'error', '@typescript-eslint/prefer-enum-initializers': 'error', '@typescript-eslint/prefer-for-of': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index de982a91d87c..66f820a0dcb4 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -78,6 +78,7 @@ import noUnusedVarsExperimental from './no-unused-vars-experimental'; import noUseBeforeDefine from './no-use-before-define'; import noUselessConstructor from './no-useless-constructor'; import noVarRequires from './no-var-requires'; +import nonNullableTypeAssertionStyle from './non-nullable-type-assertion-style'; import preferAsConst from './prefer-as-const'; import preferEnumInitializers from './prefer-enum-initializers'; import preferForOf from './prefer-for-of'; @@ -192,6 +193,7 @@ export default { 'no-use-before-define': noUseBeforeDefine, 'no-useless-constructor': noUselessConstructor, 'no-var-requires': noVarRequires, + 'non-nullable-type-assertion-style': nonNullableTypeAssertionStyle, 'prefer-as-const': preferAsConst, 'prefer-enum-initializers': preferEnumInitializers, 'prefer-for-of': preferForOf, diff --git a/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts b/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts new file mode 100644 index 000000000000..9bb099ef88cc --- /dev/null +++ b/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts @@ -0,0 +1,101 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import * as tsutils from 'tsutils'; +import * as ts from 'typescript'; + +import * as util from '../util'; + +export default util.createRule({ + name: 'non-nullable-type-assertion-style', + meta: { + docs: { + category: 'Best Practices', + description: + 'Prefers a non-null assertion over explicit type cast when possible', + recommended: false, + requiresTypeChecking: true, + suggestion: true, + }, + fixable: 'code', + messages: { + preferNonNullAssertion: + 'Use a ! assertion to more succintly remove null and undefined from the type.', + }, + schema: [], + type: 'suggestion', + }, + defaultOptions: [], + + create(context) { + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + const sourceCode = context.getSourceCode(); + + const getTypesIfNotLoose = (node: TSESTree.Node): ts.Type[] | undefined => { + const type = checker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(node), + ); + + if ( + tsutils.isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown) + ) { + return undefined; + } + + return tsutils.unionTypeParts(type); + }; + + const sameTypeWithoutNullish = ( + assertedTypes: ts.Type[], + originalTypes: ts.Type[], + ): boolean => { + const nonNullishOriginalTypes = originalTypes.filter( + type => + type.flags !== ts.TypeFlags.Null && + type.flags !== ts.TypeFlags.Undefined, + ); + + for (const assertedType of assertedTypes) { + if (!nonNullishOriginalTypes.includes(assertedType)) { + return false; + } + } + + for (const originalType of nonNullishOriginalTypes) { + if (!assertedTypes.includes(originalType)) { + return false; + } + } + + return true; + }; + + return { + 'TSAsExpression, TSTypeAssertion'( + node: TSESTree.TSTypeAssertion | TSESTree.TSAsExpression, + ): void { + const originalTypes = getTypesIfNotLoose(node.expression); + if (!originalTypes) { + return; + } + + const assertedTypes = getTypesIfNotLoose(node.typeAnnotation); + if (!assertedTypes) { + return; + } + + if (sameTypeWithoutNullish(assertedTypes, originalTypes)) { + context.report({ + fix(fixer) { + return fixer.replaceText( + node, + `${sourceCode.getText(node.expression)}!`, + ); + }, + messageId: 'preferNonNullAssertion', + node, + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts b/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts new file mode 100644 index 000000000000..45ca1774d301 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts @@ -0,0 +1,193 @@ +import path from 'path'; +import rule from '../../src/rules/non-nullable-type-assertion-style'; +import { RuleTester } from '../RuleTester'; + +const rootDir = path.resolve(__dirname, '../fixtures/'); +const ruleTester = new RuleTester({ + parserOptions: { + sourceType: 'module', + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('non-nullable-type-assertion-style', rule, { + valid: [ + ` +declare const original: number | string; +const cast = original as string; + `, + ` +declare const original: number | undefined; +const cast = original as string | number | undefined; + `, + ` +declare const original: number | any; +const cast = original as string | number | undefined; + `, + ` +declare const original: number | undefined; +const cast = original as any; + `, + ` +declare const original: number | null | undefined; +const cast = original as number | null; + `, + ` +type Type = { value: string }; +declare const original: Type | number; +const cast = original as Type; + `, + ` +type T = string; +declare const x: T | number; + +const y = x as NonNullable; + `, + ` +type T = string | null; +declare const x: T | number; + +const y = x as NonNullable; + `, + ], + + invalid: [ + { + code: ` +declare const maybe: string | undefined; +const bar = maybe as string; + `, + errors: [ + { + column: 13, + line: 3, + messageId: 'preferNonNullAssertion', + }, + ], + output: ` +declare const maybe: string | undefined; +const bar = maybe!; + `, + }, + { + code: ` +declare const maybe: string | null; +const bar = maybe as string; + `, + errors: [ + { + column: 13, + line: 3, + messageId: 'preferNonNullAssertion', + }, + ], + output: ` +declare const maybe: string | null; +const bar = maybe!; + `, + }, + { + code: ` +declare const maybe: string | null | undefined; +const bar = maybe as string; + `, + errors: [ + { + column: 13, + line: 3, + messageId: 'preferNonNullAssertion', + }, + ], + output: ` +declare const maybe: string | null | undefined; +const bar = maybe!; + `, + }, + { + code: ` +type Type = { value: string }; +declare const maybe: Type | undefined; +const bar = maybe as Type; + `, + errors: [ + { + column: 13, + line: 4, + messageId: 'preferNonNullAssertion', + }, + ], + output: ` +type Type = { value: string }; +declare const maybe: Type | undefined; +const bar = maybe!; + `, + }, + { + code: ` +interface Interface { + value: string; +} +declare const maybe: Interface | undefined; +const bar = maybe as Interface; + `, + errors: [ + { + column: 13, + line: 6, + messageId: 'preferNonNullAssertion', + }, + ], + output: ` +interface Interface { + value: string; +} +declare const maybe: Interface | undefined; +const bar = maybe!; + `, + }, + { + code: ` +type T = string | null; +declare const x: T; + +const y = x as NonNullable; + `, + errors: [ + { + column: 11, + line: 5, + messageId: 'preferNonNullAssertion', + }, + ], + output: ` +type T = string | null; +declare const x: T; + +const y = x!; + `, + }, + { + code: ` +type T = string | null | undefined; +declare const x: T; + +const y = x as NonNullable; + `, + errors: [ + { + column: 11, + line: 5, + messageId: 'preferNonNullAssertion', + }, + ], + output: ` +type T = string | null | undefined; +declare const x: T; + +const y = x!; + `, + }, + ], +});