• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import {
2  TSESTree,
3  AST_NODE_TYPES,
4} from '@typescript-eslint/experimental-utils';
5import {
6  isObjectType,
7  isObjectFlagSet,
8  isStrictCompilerOptionEnabled,
9  isTypeFlagSet,
10  isVariableDeclaration,
11} from 'tsutils';
12import * as ts from 'typescript';
13import * as util from '../util';
14
15type Options = [
16  {
17    typesToIgnore?: string[];
18  },
19];
20type MessageIds = 'contextuallyUnnecessary' | 'unnecessaryAssertion';
21
22export default util.createRule<Options, MessageIds>({
23  name: 'no-unnecessary-type-assertion',
24  meta: {
25    docs: {
26      description:
27        'Warns if a type assertion does not change the type of an expression',
28      category: 'Best Practices',
29      recommended: 'error',
30      requiresTypeChecking: true,
31    },
32    fixable: 'code',
33    messages: {
34      unnecessaryAssertion:
35        'This assertion is unnecessary since it does not change the type of the expression.',
36      contextuallyUnnecessary:
37        'This assertion is unnecessary since the receiver accepts the original type of the expression.',
38    },
39    schema: [
40      {
41        type: 'object',
42        properties: {
43          typesToIgnore: {
44            type: 'array',
45            items: {
46              type: 'string',
47            },
48          },
49        },
50      },
51    ],
52    type: 'suggestion',
53  },
54  defaultOptions: [{}],
55  create(context, [options]) {
56    const sourceCode = context.getSourceCode();
57    const parserServices = util.getParserServices(context);
58    const checker = parserServices.program.getTypeChecker();
59    const compilerOptions = parserServices.program.getCompilerOptions();
60
61    /**
62     * Sometimes tuple types don't have ObjectFlags.Tuple set, like when they're being matched against an inferred type.
63     * So, in addition, check if there are integer properties 0..n and no other numeric keys
64     */
65    function couldBeTupleType(type: ts.ObjectType): boolean {
66      const properties = type.getProperties();
67
68      if (properties.length === 0) {
69        return false;
70      }
71      let i = 0;
72
73      for (; i < properties.length; ++i) {
74        const name = properties[i].name;
75
76        if (String(i) !== name) {
77          if (i === 0) {
78            // if there are no integer properties, this is not a tuple
79            return false;
80          }
81          break;
82        }
83      }
84      for (; i < properties.length; ++i) {
85        if (String(+properties[i].name) === properties[i].name) {
86          return false; // if there are any other numeric properties, this is not a tuple
87        }
88      }
89      return true;
90    }
91
92    /**
93     * Returns true if there's a chance the variable has been used before a value has been assigned to it
94     */
95    function isPossiblyUsedBeforeAssigned(node: ts.Expression): boolean {
96      const declaration = util.getDeclaration(checker, node);
97      if (!declaration) {
98        // don't know what the declaration is for some reason, so just assume the worst
99        return true;
100      }
101
102      if (
103        // non-strict mode doesn't care about used before assigned errors
104        isStrictCompilerOptionEnabled(compilerOptions, 'strictNullChecks') &&
105        // ignore class properties as they are compile time guarded
106        // also ignore function arguments as they can't be used before defined
107        isVariableDeclaration(declaration) &&
108        // is it `const x!: number`
109        declaration.initializer === undefined &&
110        declaration.exclamationToken === undefined &&
111        declaration.type !== undefined
112      ) {
113        // check if the defined variable type has changed since assignment
114        const declarationType = checker.getTypeFromTypeNode(declaration.type);
115        const type = util.getConstrainedTypeAtLocation(checker, node);
116        if (declarationType === type) {
117          // possibly used before assigned, so just skip it
118          // better to false negative and skip it, than false positive and fix to compile erroring code
119          //
120          // no better way to figure this out right now
121          // https://github.com/Microsoft/TypeScript/issues/31124
122          return true;
123        }
124      }
125      return false;
126    }
127
128    function isConstAssertion(node: TSESTree.TypeNode): boolean {
129      return (
130        node.type === AST_NODE_TYPES.TSTypeReference &&
131        node.typeName.type === AST_NODE_TYPES.Identifier &&
132        node.typeName.name === 'const'
133      );
134    }
135
136    return {
137      TSNonNullExpression(node): void {
138        const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
139        const type = util.getConstrainedTypeAtLocation(
140          checker,
141          originalNode.expression,
142        );
143
144        if (!util.isNullableType(type)) {
145          if (isPossiblyUsedBeforeAssigned(originalNode.expression)) {
146            return;
147          }
148
149          context.report({
150            node,
151            messageId: 'unnecessaryAssertion',
152            fix(fixer) {
153              return fixer.removeRange([
154                originalNode.expression.end,
155                originalNode.end,
156              ]);
157            },
158          });
159        } else {
160          // we know it's a nullable type
161          // so figure out if the variable is used in a place that accepts nullable types
162
163          const contextualType = util.getContextualType(checker, originalNode);
164          if (contextualType) {
165            // in strict mode you can't assign null to undefined, so we have to make sure that
166            // the two types share a nullable type
167            const typeIncludesUndefined = util.isTypeFlagSet(
168              type,
169              ts.TypeFlags.Undefined,
170            );
171            const typeIncludesNull = util.isTypeFlagSet(
172              type,
173              ts.TypeFlags.Null,
174            );
175
176            const contextualTypeIncludesUndefined = util.isTypeFlagSet(
177              contextualType,
178              ts.TypeFlags.Undefined,
179            );
180            const contextualTypeIncludesNull = util.isTypeFlagSet(
181              contextualType,
182              ts.TypeFlags.Null,
183            );
184
185            // make sure that the parent accepts the same types
186            // i.e. assigning `string | null | undefined` to `string | undefined` is invalid
187            const isValidUndefined = typeIncludesUndefined
188              ? contextualTypeIncludesUndefined
189              : true;
190            const isValidNull = typeIncludesNull
191              ? contextualTypeIncludesNull
192              : true;
193
194            if (isValidUndefined && isValidNull) {
195              context.report({
196                node,
197                messageId: 'contextuallyUnnecessary',
198                fix(fixer) {
199                  return fixer.removeRange([
200                    originalNode.expression.end,
201                    originalNode.end,
202                  ]);
203                },
204              });
205            }
206          }
207        }
208      },
209      'TSAsExpression, TSTypeAssertion'(
210        node: TSESTree.TSTypeAssertion | TSESTree.TSAsExpression,
211      ): void {
212        if (
213          options.typesToIgnore?.includes(
214            sourceCode.getText(node.typeAnnotation),
215          ) ||
216          isConstAssertion(node.typeAnnotation)
217        ) {
218          return;
219        }
220
221        const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
222        const castType = checker.getTypeAtLocation(originalNode);
223
224        if (
225          isTypeFlagSet(castType, ts.TypeFlags.Literal) ||
226          (isObjectType(castType) &&
227            (isObjectFlagSet(castType, ts.ObjectFlags.Tuple) ||
228              couldBeTupleType(castType)))
229        ) {
230          // It's not always safe to remove a cast to a literal type or tuple
231          // type, as those types are sometimes widened without the cast.
232          return;
233        }
234
235        const uncastType = checker.getTypeAtLocation(originalNode.expression);
236
237        if (uncastType === castType) {
238          context.report({
239            node,
240            messageId: 'unnecessaryAssertion',
241            fix(fixer) {
242              return originalNode.kind === ts.SyntaxKind.TypeAssertionExpression
243                ? fixer.removeRange([
244                    node.range[0],
245                    node.expression.range[0] - 1,
246                  ])
247                : fixer.removeRange([
248                    node.expression.range[1] + 1,
249                    node.range[1],
250                  ]);
251            },
252          });
253        }
254
255        // TODO - add contextually unnecessary check for this
256      },
257    };
258  },
259});
260