Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(eslint-plugin): add rule non-nullable-type-assertion-style #2624

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | | | |
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = <string>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.
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
101 changes: 101 additions & 0 deletions packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
},
};
},
});
Original file line number Diff line number Diff line change
@@ -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<T>;
`,
`
type T = string | null;
declare const x: T | number;

const y = x as NonNullable<T>;
`,
],

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<T>;
`,
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<T>;
`,
errors: [
{
column: 11,
line: 5,
messageId: 'preferNonNullAssertion',
},
],
output: `
type T = string | null | undefined;
declare const x: T;

const y = x!;
`,
},
],
});