Skip to content

⬅️ Back to Table of Contents

📄 no-unnecessary-type-conversion

📊 Analysis Summary

Metric Count
🔧 Functions 5
📦 Imports 10
📑 Type Aliases 2

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts

📤 Default Export

export default createRule<Options, MessageIds>({ ... })
Property Value
name 'no-unnecessary-type-conversion'
meta.type 'suggestion'
meta.docs.description 'Disallow conversion idioms when they do not change the type or value of the expression'
meta.docs.recommended 'strict'
meta.docs.requiresTypeChecking true
meta.hasSuggestions true
meta.messages.suggestRemove 'Remove the type conversion.'
meta.messages.suggestSatisfies 'Instead, assert that the value satisfies the {{type}} type.'
meta.messages.unnecessaryTypeConversion '{{violation}} does not change the type or value of the {{type}}.'
meta.schema []
defaultOptions []

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESTree @typescript-eslint/utils
RuleFix @typescript-eslint/utils/ts-eslint
RuleFixer @typescript-eslint/utils/ts-eslint
AST_NODE_TYPES @typescript-eslint/utils
ASTUtils @typescript-eslint/utils
createRule ../util
getConstrainedTypeAtLocation ../util
getParserServices ../util
getWrappingFixer ../util
isTypeFlagSet ../util

Functions

create(context: any): { 'AssignmentExpression[operator = "+="]'(node: TSESTree.As…

Parameters:

  • context any

Returns: { 'AssignmentExpression[operator = "+="]'(node: TSESTree.AssignmentExpression): void; 'BinaryExpression[operator = "+"]'(node: TSESTree.BinaryExpression): void; CallExpression(node: TSESTree.CallExpression): void; 'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(node: TSESTree.Expression): void; 'UnaryExpression[operator = "!"] > UnaryExpression[operator = "!"]'(node: TSESTree.UnaryExpression): void; 'UnaryExpression[operator = "+"]'(node: TSESTree.UnaryExpression): void; 'UnaryExpression[operator = "~"] > UnaryExpression[operator = "~"]'(node: TSESTree.UnaryExpression): void; }

Calls:

  • tsutils .unionConstituents(type) .every
  • isTypeFlagSet (from ../util)
  • getParserServices (from ../util)
  • services.getTypeAtLocation
  • doesUnderlyingTypeMatchFlag
  • context.report
  • getWrappingFixer (from ../util)
  • fixer.removeRange
  • context.sourceCode.getScope
  • ASTUtils.findVariable
  • getConstrainedTypeAtLocation (from ../util)
  • nodeCallee.name.toLowerCase
  • isEnumType
  • isEnumMemberType
  • handleUnaryOperator
  • tsutils.unionConstituents(type).every
  • Number.isInteger
Code
create(context) {
    function doesUnderlyingTypeMatchFlag(
      type: ts.Type,
      typeFlag: ts.TypeFlags,
    ): boolean {
      return tsutils
        .unionConstituents(type)
        .every(t => isTypeFlagSet(t, typeFlag));
    }

    const services = getParserServices(context);

    function handleUnaryOperator(
      node: TSESTree.UnaryExpression,
      typeFlag: ts.TypeFlags,
      typeString: 'boolean' | 'number',
      violation: string,
      isDoubleOperator: boolean, // !!
    ) {
      const outerNode = isDoubleOperator ? node.parent : node;
      const type = services.getTypeAtLocation(node.argument);
      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
        const wrappingFixerParams = {
          node: outerNode,
          innerNode: [node.argument],
          sourceCode: context.sourceCode,
        };

        context.report({
          loc: {
            start: outerNode.loc.start,
            end: {
              column: node.loc.start.column + 1,
              line: node.loc.start.line,
            },
          },
          messageId: 'unnecessaryTypeConversion',
          data: { type: typeString, violation },
          suggest: [
            {
              messageId: 'suggestRemove',
              fix: getWrappingFixer(wrappingFixerParams),
            },
            {
              messageId: 'suggestSatisfies',
              data: { type: typeString },
              fix: getWrappingFixer({
                ...wrappingFixerParams,
                wrap: expr => `${expr} satisfies ${typeString}`,
              }),
            },
          ],
        });
      }
    }

    return {
      'AssignmentExpression[operator = "+="]'(
        node: TSESTree.AssignmentExpression,
      ): void {
        if (
          node.right.type === AST_NODE_TYPES.Literal &&
          node.right.value === '' &&
          doesUnderlyingTypeMatchFlag(
            services.getTypeAtLocation(node.left),
            ts.TypeFlags.StringLike,
          )
        ) {
          const wrappingFixerParams = {
            node,
            innerNode: [node.left],
            sourceCode: context.sourceCode,
          };

          context.report({
            node,
            messageId: 'unnecessaryTypeConversion',
            data: {
              type: 'string',
              violation: "Concatenating a string with ''",
            },
            suggest: [
              {
                messageId: 'suggestRemove',
                fix:
                  node.parent.type === AST_NODE_TYPES.ExpressionStatement
                    ? (fixer: RuleFixer): RuleFix[] => [
                        fixer.removeRange([
                          node.parent.range[0],
                          node.parent.range[1],
                        ]),
                      ]
                    : getWrappingFixer(wrappingFixerParams),
              },
              {
                messageId: 'suggestSatisfies',
                data: { type: 'string' },
                fix: getWrappingFixer({
                  ...wrappingFixerParams,
                  wrap: expr => `${expr} satisfies string`,
                }),
              },
            ],
          });
        }
      },
      'BinaryExpression[operator = "+"]'(
        node: TSESTree.BinaryExpression,
      ): void {
        if (
          node.right.type === AST_NODE_TYPES.Literal &&
          node.right.value === '' &&
          doesUnderlyingTypeMatchFlag(
            services.getTypeAtLocation(node.left),
            ts.TypeFlags.StringLike,
          )
        ) {
          const wrappingFixerParams = {
            node,
            innerNode: [node.left],
            sourceCode: context.sourceCode,
          };

          context.report({
            loc: {
              start: node.left.loc.end,
              end: node.loc.end,
            },
            messageId: 'unnecessaryTypeConversion',
            data: {
              type: 'string',
              violation: "Concatenating a string with ''",
            },
            suggest: [
              {
                messageId: 'suggestRemove',
                fix: getWrappingFixer(wrappingFixerParams),
              },
              {
                messageId: 'suggestSatisfies',
                data: { type: 'string' },
                fix: getWrappingFixer({
                  ...wrappingFixerParams,
                  wrap: expr => `${expr} satisfies string`,
                }),
              },
            ],
          });
        } else if (
          node.left.type === AST_NODE_TYPES.Literal &&
          node.left.value === '' &&
          doesUnderlyingTypeMatchFlag(
            services.getTypeAtLocation(node.right),
            ts.TypeFlags.StringLike,
          )
        ) {
          const wrappingFixerParams = {
            node,
            innerNode: [node.right],
            sourceCode: context.sourceCode,
          };

          context.report({
            loc: {
              start: node.loc.start,
              end: node.right.loc.start,
            },
            messageId: 'unnecessaryTypeConversion',
            data: {
              type: 'string',
              violation: "Concatenating '' with a string",
            },
            suggest: [
              {
                messageId: 'suggestRemove',
                fix: getWrappingFixer(wrappingFixerParams),
              },
              {
                messageId: 'suggestSatisfies',
                data: { type: 'string' },
                fix: getWrappingFixer({
                  ...wrappingFixerParams,
                  wrap: expr => `${expr} satisfies string`,
                }),
              },
            ],
          });
        }
      },
      CallExpression(node: TSESTree.CallExpression): void {
        const nodeCallee = node.callee;
        const builtInTypeFlags = {
          BigInt: ts.TypeFlags.BigIntLike,
          Boolean: ts.TypeFlags.BooleanLike,
          Number: ts.TypeFlags.NumberLike,
          String: ts.TypeFlags.StringLike,
        };

        if (
          nodeCallee.type !== AST_NODE_TYPES.Identifier ||
          !(nodeCallee.name in builtInTypeFlags)
        ) {
          return;
        }

        const typeFlag =
          builtInTypeFlags[nodeCallee.name as keyof typeof builtInTypeFlags];
        const scope = context.sourceCode.getScope(node);
        const variable = ASTUtils.findVariable(scope, nodeCallee.name);
        if (
          !!variable?.defs.length ||
          !doesUnderlyingTypeMatchFlag(
            getConstrainedTypeAtLocation(services, node.arguments[0]),
            typeFlag,
          )
        ) {
          return;
        }

        const wrappingFixerParams = {
          node,
          innerNode: [node.arguments[0]],
          sourceCode: context.sourceCode,
        };
        const typeString = nodeCallee.name.toLowerCase();

        context.report({
          node: nodeCallee,
          messageId: 'unnecessaryTypeConversion',
          data: {
            type: nodeCallee.name.toLowerCase(),
            violation: `Passing a ${typeString} to ${nodeCallee.name}()`,
          },
          suggest: [
            {
              messageId: 'suggestRemove',
              fix: getWrappingFixer(wrappingFixerParams),
            },
            {
              messageId: 'suggestSatisfies',
              data: { type: typeString },
              fix: getWrappingFixer({
                ...wrappingFixerParams,
                wrap: expr => `${expr} satisfies ${typeString}`,
              }),
            },
          ],
        });
      },
      'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
        node: TSESTree.Expression,
      ): void {
        const memberExpr = node.parent as TSESTree.MemberExpression;
        const type = getConstrainedTypeAtLocation(services, memberExpr.object);

        if (isEnumType(type) || isEnumMemberType(type)) {
          return;
        }

        if (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike)) {
          const wrappingFixerParams = {
            node: memberExpr.parent,
            innerNode: [memberExpr.object],
            sourceCode: context.sourceCode,
          };

          context.report({
            loc: {
              start: memberExpr.property.loc.start,
              end: memberExpr.parent.loc.end,
            },
            messageId: 'unnecessaryTypeConversion',
            data: {
              type: 'string',
              violation: "Calling a string's .toString() method",
            },
            suggest: [
              {
                messageId: 'suggestRemove',
                fix: getWrappingFixer(wrappingFixerParams),
              },
              {
                messageId: 'suggestSatisfies',
                data: { type: 'string' },
                fix: getWrappingFixer({
                  ...wrappingFixerParams,
                  wrap: expr => `${expr} satisfies string`,
                }),
              },
            ],
          });
        }
      },
      'UnaryExpression[operator = "!"] > UnaryExpression[operator = "!"]'(
        node: TSESTree.UnaryExpression,
      ): void {
        handleUnaryOperator(
          node,
          ts.TypeFlags.BooleanLike,
          'boolean',
          'Using !! on a boolean',
          true,
        );
      },
      'UnaryExpression[operator = "+"]'(node: TSESTree.UnaryExpression): void {
        handleUnaryOperator(
          node,
          ts.TypeFlags.NumberLike,
          'number',
          'Using the unary + operator on a number',
          false,
        );
      },
      'UnaryExpression[operator = "~"] > UnaryExpression[operator = "~"]'(
        node: TSESTree.UnaryExpression,
      ): void {
        const outerNode = node.parent;
        const type = services.getTypeAtLocation(node.argument);

        if (
          tsutils.unionConstituents(type).every(t => {
            return (
              isTypeFlagSet(t, ts.TypeFlags.NumberLiteral) &&
              Number.isInteger((t as ts.NumberLiteralType).value)
            );
          })
        ) {
          const wrappingFixerParams = {
            node: outerNode,
            innerNode: [node.argument],
            sourceCode: context.sourceCode,
          };

          context.report({
            loc: {
              start: outerNode.loc.start,
              end: {
                column: node.loc.start.column + 1,
                line: node.loc.start.line,
              },
            },
            messageId: 'unnecessaryTypeConversion',
            data: { type: 'number', violation: 'Using ~~ on an integer' },
            suggest: [
              {
                messageId: 'suggestRemove',
                fix: getWrappingFixer(wrappingFixerParams),
              },
              {
                messageId: 'suggestSatisfies',
                data: { type: 'number' },
                fix: getWrappingFixer({
                  ...wrappingFixerParams,
                  wrap: expr => `${expr} satisfies number`,
                }),
              },
            ],
          });
        }
      },
    };
  }

isEnumType(type: ts.Type): boolean

Parameters:

  • type ts.Type

Returns: boolean

Calls:

  • type.getFlags
Code
function isEnumType(type: ts.Type): boolean {
  return (type.getFlags() & ts.TypeFlags.EnumLike) !== 0;
}

isEnumMemberType(type: ts.Type): boolean

Parameters:

  • type ts.Type

Returns: boolean

Calls:

  • type.getSymbol
  • tsutils.isSymbolFlagSet
Code
function isEnumMemberType(type: ts.Type): boolean {
  const symbol = type.getSymbol();
  if (!symbol) {
    return false;
  }
  return tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.EnumMember);
}

Internal helpers

Declared inside another function in this file.

doesUnderlyingTypeMatchFlag(type: ts.Type, typeFlag: ts.TypeFlags): boolean

Parameters:

  • type ts.Type
  • typeFlag ts.TypeFlags

Returns: boolean

Calls:

  • tsutils .unionConstituents(type) .every
  • isTypeFlagSet (from ../util)
Code
function doesUnderlyingTypeMatchFlag(
      type: ts.Type,
      typeFlag: ts.TypeFlags,
    ): boolean {
      return tsutils
        .unionConstituents(type)
        .every(t => isTypeFlagSet(t, typeFlag));
    }

handleUnaryOperator(node: TSESTree.UnaryExpression, typeFlag: ts.TypeFlags, typeString: 'boolean' | 'number', violation: string, isDoubleOperator: boolean): void

Parameters:

  • node TSESTree.UnaryExpression
  • typeFlag ts.TypeFlags
  • typeString 'boolean' | 'number'
  • violation string
  • isDoubleOperator boolean

Returns: void

Calls:

  • services.getTypeAtLocation
  • doesUnderlyingTypeMatchFlag
  • context.report
  • getWrappingFixer (from ../util)
Code
function handleUnaryOperator(
      node: TSESTree.UnaryExpression,
      typeFlag: ts.TypeFlags,
      typeString: 'boolean' | 'number',
      violation: string,
      isDoubleOperator: boolean, // !!
    ) {
      const outerNode = isDoubleOperator ? node.parent : node;
      const type = services.getTypeAtLocation(node.argument);
      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
        const wrappingFixerParams = {
          node: outerNode,
          innerNode: [node.argument],
          sourceCode: context.sourceCode,
        };

        context.report({
          loc: {
            start: outerNode.loc.start,
            end: {
              column: node.loc.start.column + 1,
              line: node.loc.start.line,
            },
          },
          messageId: 'unnecessaryTypeConversion',
          data: { type: typeString, violation },
          suggest: [
            {
              messageId: 'suggestRemove',
              fix: getWrappingFixer(wrappingFixerParams),
            },
            {
              messageId: 'suggestSatisfies',
              data: { type: typeString },
              fix: getWrappingFixer({
                ...wrappingFixerParams,
                wrap: expr => `${expr} satisfies ${typeString}`,
              }),
            },
          ],
        });
      }
    }

Type Aliases

Options

type Options = [];

MessageIds

type MessageIds = 'suggestRemove' | 'suggestSatisfies' | 'unnecessaryTypeConversion';

Generated by Syntax Scribe