Skip to content

⬅️ Back to Table of Contents

📄 no-unnecessary-type-parameters

📊 Analysis Summary

Metric Count
🔧 Functions 14
📦 Imports 10
📐 Interfaces 2
📑 Type Aliases 1

📚 Table of Contents

🛠️ File Location:

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

📤 Default Export

export default createRule({ ... })
Property Value
name 'no-unnecessary-type-parameters'
meta.type 'problem'
meta.docs.description "Disallow type parameters that aren't used multiple times"
meta.docs.recommended 'strict'
meta.docs.requiresTypeChecking true
meta.hasSuggestions true
meta.messages.replaceUsagesWithConstraint 'Replace all usages of type parameter with its constraint.'
meta.messages.sole 'Type parameter {{name}} is {{uses}} in the {{descriptor}} signature.'
meta.schema []
defaultOptions []

Entry point: create — documented under Functions.


📦 Imports

Name Source
Reference @typescript-eslint/scope-manager
TSESLint @typescript-eslint/utils
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
MakeRequired ../util
createRule ../util
getParserServices ../util
getWrappingFixer ../util
nullThrows ../util
NullThrowsReasons ../util

Functions

create(context: any): { [x: string]: (node: TSESTree.FunctionLike) => void; }

Parameters:

  • context any

Returns: { [x: string]: (node: TSESTree.FunctionLike) => void; }

Calls:

  • getParserServices (from ../util)
  • parserServices.esTreeNodeToTSNodeMap.get
  • parserServices.program.getTypeChecker
  • context.sourceCode.getScope
  • parserServices.tsNodeToESTreeNodeMap.get
  • nullThrows (from ../util)
  • complex_call_1882
  • scope.set.get
  • isTypeParameterRepeatedInAST
  • countTypeParameterUsage
  • counts.get
  • context.report
  • context.sourceCode.getText
  • [ AST_NODE_TYPES.TSArrayType, AST_NODE_TYPES.TSIndexedAccessType, AST_NODE_TYPES.TSIntersectionType, AST_NODE_TYPES.TSUnionType, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ].some
  • complex_call_4700
  • fixer.replaceText
  • fixer.remove
  • typeParamsNode.params.indexOf
  • context.sourceCode.getTokenAfter
  • NullThrowsReasons.MissingToken
  • fixer.removeRange
  • context.sourceCode.getTokenBefore
  • [ 'ArrowFunctionExpression[typeParameters]', 'FunctionDeclaration[typeParameters]', 'FunctionExpression[typeParameters]', 'TSCallSignatureDeclaration[typeParameters]', 'TSConstructorType[typeParameters]', 'TSDeclareFunction[typeParameters]', 'TSEmptyBodyFunctionExpression[typeParameters]', 'TSFunctionType[typeParameters]', 'TSMethodSignature[typeParameters]', ].join
  • checkNode
  • [ 'ClassDeclaration[typeParameters]', 'ClassExpression[typeParameters]', ].join

Internal Comments:

// Get the scope in which the type parameters are declared. (x2)
// Quick path: if the type parameter is used multiple times in the AST,
// we don't need to dip into types to know it's repeated.
// For any inferred types, we have to dip into type checking. (x3)
// Replace all the usages of the type parameter with the constraint... (x2)
// special case - a constraint of 'any' actually acts like 'unknown' (x2)
// ...and remove the type parameter itself from the declaration. (x2)
// We are assuming at this point that the reported type parameter
// is present in the inspected node's type parameters.
// Remove the whole <T> generic syntax if we're removing the only type parameter in the list. (x2)

Code
create(context) {
    const parserServices = getParserServices(context);

    function checkNode(node: TSESTree.FunctionLike, descriptor: string): void {
      const tsNode = parserServices.esTreeNodeToTSNodeMap.get(
        node,
      ) as NodeWithTypeParameters;

      const checker = parserServices.program.getTypeChecker();
      let counts: Map<ts.Identifier, number> | undefined;

      // Get the scope in which the type parameters are declared.
      const scope = context.sourceCode.getScope(node);

      for (const typeParameter of tsNode.typeParameters) {
        const esTypeParameter =
          parserServices.tsNodeToESTreeNodeMap.get<TSESTree.TSTypeParameter>(
            typeParameter,
          );

        const smTypeParameterVariable = nullThrows(
          (() => {
            const variable = scope.set.get(esTypeParameter.name.name);
            return variable?.isTypeVariable ? variable : undefined;
          })(),
          "Type parameter should be present in scope's variables.",
        );

        // Quick path: if the type parameter is used multiple times in the AST,
        // we don't need to dip into types to know it's repeated.
        if (
          isTypeParameterRepeatedInAST(
            esTypeParameter,
            smTypeParameterVariable.references,
            node.body?.range[0] ?? node.returnType?.range[1],
          )
        ) {
          continue;
        }

        // For any inferred types, we have to dip into type checking.
        counts ??= countTypeParameterUsage(checker, tsNode);
        const identifierCounts = counts.get(typeParameter.name);
        if (!identifierCounts || identifierCounts > 2) {
          continue;
        }

        context.report({
          node: esTypeParameter,
          messageId: 'sole',
          data: {
            name: typeParameter.name.text,
            descriptor,
            uses: identifierCounts === 1 ? 'never used' : 'used only once',
          },
          suggest: [
            {
              messageId: 'replaceUsagesWithConstraint',
              *fix(fixer): Generator<TSESLint.RuleFix> {
                // Replace all the usages of the type parameter with the constraint...

                const constraint = esTypeParameter.constraint;
                // special case - a constraint of 'any' actually acts like 'unknown'
                const constraintText =
                  constraint != null &&
                  constraint.type !== AST_NODE_TYPES.TSAnyKeyword
                    ? context.sourceCode.getText(constraint)
                    : 'unknown';
                for (const reference of smTypeParameterVariable.references) {
                  if (reference.isTypeReference) {
                    const referenceNode = reference.identifier;
                    const isComplexType =
                      constraint?.type === AST_NODE_TYPES.TSUnionType ||
                      constraint?.type === AST_NODE_TYPES.TSIntersectionType ||
                      constraint?.type === AST_NODE_TYPES.TSConditionalType;
                    const hasMatchingAncestorType = [
                      AST_NODE_TYPES.TSArrayType,
                      AST_NODE_TYPES.TSIndexedAccessType,
                      AST_NODE_TYPES.TSIntersectionType,
                      AST_NODE_TYPES.TSUnionType,
                      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    ].some(type => referenceNode.parent.parent!.type === type);
                    if (isComplexType && hasMatchingAncestorType) {
                      const fixResult = getWrappingFixer({
                        node: referenceNode,
                        innerNode: constraint,
                        sourceCode: context.sourceCode,
                        wrap: constraintNode => constraintNode,
                      })(fixer);
                      yield fixResult;
                    } else {
                      yield fixer.replaceText(referenceNode, constraintText);
                    }
                  }
                }

                // ...and remove the type parameter itself from the declaration.

                const typeParamsNode = nullThrows(
                  node.typeParameters,
                  'node should have type parameters',
                );

                // We are assuming at this point that the reported type parameter
                // is present in the inspected node's type parameters.
                if (typeParamsNode.params.length === 1) {
                  // Remove the whole <T> generic syntax if we're removing the only type parameter in the list.
                  yield fixer.remove(typeParamsNode);
                } else {
                  const index = typeParamsNode.params.indexOf(esTypeParameter);

                  if (index === 0) {
                    const commaAfter = nullThrows(
                      context.sourceCode.getTokenAfter(
                        esTypeParameter,
                        token => token.value === ',',
                      ),
                      NullThrowsReasons.MissingToken(
                        'comma',
                        'type parameter list',
                      ),
                    );

                    const tokenAfterComma = nullThrows(
                      context.sourceCode.getTokenAfter(commaAfter, {
                        includeComments: true,
                      }),
                      NullThrowsReasons.MissingToken(
                        'token',
                        'type parameter list',
                      ),
                    );

                    yield fixer.removeRange([
                      esTypeParameter.range[0],
                      tokenAfterComma.range[0],
                    ]);
                  } else {
                    const commaBefore = nullThrows(
                      context.sourceCode.getTokenBefore(
                        esTypeParameter,
                        token => token.value === ',',
                      ),
                      NullThrowsReasons.MissingToken(
                        'comma',
                        'type parameter list',
                      ),
                    );

                    yield fixer.removeRange([
                      commaBefore.range[0],
                      esTypeParameter.range[1],
                    ]);
                  }
                }
              },
            },
          ],
        });
      }
    }

    return {
      [[
        'ArrowFunctionExpression[typeParameters]',
        'FunctionDeclaration[typeParameters]',
        'FunctionExpression[typeParameters]',
        'TSCallSignatureDeclaration[typeParameters]',
        'TSConstructorType[typeParameters]',
        'TSDeclareFunction[typeParameters]',
        'TSEmptyBodyFunctionExpression[typeParameters]',
        'TSFunctionType[typeParameters]',
        'TSMethodSignature[typeParameters]',
      ].join(', ')](node: TSESTree.FunctionLike): void {
        checkNode(node, 'function');
      },
      [[
        'ClassDeclaration[typeParameters]',
        'ClassExpression[typeParameters]',
      ].join(', ')](node: TSESTree.FunctionLike): void {
        checkNode(node, 'class');
      },
    };
  }

isTypeParameterRepeatedInAST(node: TSESTree.TSTypeParameter, references: Reference[], startOfBody: number): boolean

Parameters:

  • node TSESTree.TSTypeParameter
  • references Reference[]
  • startOfBody number

Returns: boolean

Calls:

  • skipConstituentsUpward
  • grandparent.params.includes
  • ['Array', 'ReadonlyArray'].includes

Internal Comments:

// References inside the type parameter's definition don't count...
// ...nor references that are outside the declaring signature.
// Neither do references that aren't to the same type parameter,
// namely value-land (non-type) identifiers of the type parameter's type,
// and references to different type parameters or values.
// If the type parameter is being used as a type argument, then we
// know the type parameter is being reused and can't be reported.
// Array and ReadonlyArray must be handled carefully
// let's defer the check to the type-aware phase

Code
function isTypeParameterRepeatedInAST(
  node: TSESTree.TSTypeParameter,
  references: Reference[],
  startOfBody = Infinity,
): boolean {
  let total = 0;

  for (const reference of references) {
    // References inside the type parameter's definition don't count...
    if (
      reference.identifier.range[0] < node.range[1] &&
      reference.identifier.range[1] > node.range[0]
    ) {
      continue;
    }

    // ...nor references that are outside the declaring signature.
    if (reference.identifier.range[0] > startOfBody) {
      continue;
    }

    // Neither do references that aren't to the same type parameter,
    // namely value-land (non-type) identifiers of the type parameter's type,
    // and references to different type parameters or values.
    if (
      !reference.isTypeReference ||
      reference.identifier.name !== node.name.name
    ) {
      continue;
    }

    // If the type parameter is being used as a type argument, then we
    // know the type parameter is being reused and can't be reported.
    if (reference.identifier.parent.type === AST_NODE_TYPES.TSTypeReference) {
      const grandparent = skipConstituentsUpward(
        reference.identifier.parent.parent,
      );

      if (
        grandparent.type === AST_NODE_TYPES.TSTypeParameterInstantiation &&
        grandparent.params.includes(reference.identifier.parent) &&
        // Array and ReadonlyArray must be handled carefully
        // let's defer the check to the type-aware phase
        !(
          grandparent.parent.type === AST_NODE_TYPES.TSTypeReference &&
          grandparent.parent.typeName.type === AST_NODE_TYPES.Identifier &&
          ['Array', 'ReadonlyArray'].includes(grandparent.parent.typeName.name)
        )
      ) {
        return true;
      }
    }

    total += 1;

    if (total >= 2) {
      return true;
    }
  }

  return false;
}

skipConstituentsUpward(node: TSESTree.Node): TSESTree.Node

Parameters:

  • node TSESTree.Node

Returns: TSESTree.Node

Calls:

  • skipConstituentsUpward
Code
function skipConstituentsUpward(node: TSESTree.Node): TSESTree.Node {
  switch (node.type) {
    case AST_NODE_TYPES.TSIntersectionType:
    case AST_NODE_TYPES.TSUnionType:
      return skipConstituentsUpward(node.parent);
    default:
      return node;
  }
}

countTypeParameterUsage(checker: ts.TypeChecker, node: NodeWithTypeParameters): Map<ts.Identifier, number>

Count uses of type parameters in inferred return types. We need to resolve and analyze the inferred return type of a function to see whether it contains additional references to the type parameters. For classes, we need to do this for all their methods.

Raw JSDoc
/**
 * Count uses of type parameters in inferred return types.
 * We need to resolve and analyze the inferred return type of a function
 * to see whether it contains additional references to the type parameters.
 * For classes, we need to do this for all their methods.
 */

Calls:

  • ts.isClassLike
  • collectTypeParameterUsageCounts
Code
function countTypeParameterUsage(
  checker: ts.TypeChecker,
  node: NodeWithTypeParameters,
): Map<ts.Identifier, number> {
  const counts = new Map<ts.Identifier, number>();

  if (ts.isClassLike(node)) {
    for (const typeParameter of node.typeParameters) {
      collectTypeParameterUsageCounts(checker, typeParameter, counts, true);
    }
    for (const member of node.members) {
      collectTypeParameterUsageCounts(checker, member, counts, true);
    }
  } else {
    collectTypeParameterUsageCounts(checker, node, counts, false);
  }

  return counts;
}

collectTypeParameterUsageCounts(checker: ts.TypeChecker, node: ts.Node, foundIdentifierUsages: Map<ts.Identifier, number>, fromClass: boolean): void

Populates by the number of times each type parameter appears in the given type by checking its uses through its type references. This is essentially a limited subset of the scope manager, but for types.

Raw JSDoc
/**
 * Populates {@link foundIdentifierUsages} by the number of times each type parameter
 * appears in the given type by checking its uses through its type references.
 * This is essentially a limited subset of the scope manager, but for types.
 */

Calls:

  • checker.getTypeAtLocation
  • ts.isCallSignatureDeclaration
  • ts.isConstructorDeclaration
  • visitSignature
  • checker.getSignatureFromDeclaration
  • visitType
  • incrementTypeUsages
  • tsutils.isTypeParameter
  • type.getSymbol()?.getDeclarations
  • incrementIdentifierCount
  • visitedConstraints.has
  • visitedConstraints.add
  • visitTypesList
  • tsutils.isUnionOrIntersectionType
  • tsutils.isIndexedAccessType
  • tsutils.isTypeReference
  • tsutils.isTupleType
  • checker.isArrayType
  • (type.symbol as ts.Symbol | undefined)?.getName
  • tsutils.isTemplateLiteralType
  • tsutils.isConditionalType
  • tsutils.isObjectType
  • type.getProperties
  • visitSymbolsListOnce
  • isMappedType
  • type.getNumberIndexType
  • type.getStringIndexType
  • type.getCallSignatures().forEach
  • type.getConstructSignatures().forEach
  • isOperatorType
  • foundIdentifierUsages.get
  • foundIdentifierUsages.set
  • typeUsages.get
  • typeUsages.set
  • checker.getTypeOfSymbol
  • signature.getTypeParameters
  • checker.getTypePredicateOfSignature
  • signature.getReturnType
  • visitedSymbolLists.has
  • visitedSymbolLists.add

Internal Comments:

// Seeing the same type > (threshold=3 ** 2) times indicates a likely
// recursive type, like `type T = { [P in keyof T]: T }`.
// If it's not recursive, then heck, we've seen it enough times that any
// referenced types have been counted enough to qualify as used.
// Visiting the type of a constrained type parameter will recurse into
// the constraint. We avoid infinite loops by visiting each only once.
// We don't descend into the definition of the type alias, so we don't (x3)
// know whether it's used multiple times. It's safest to assume it is. (x3)
// currently, if we are in a "class context", everything is accepted (x2)
// special cases - readonly arrays/tuples are considered only to use the (x3)
// type parameter once. Mutable arrays/tuples are considered to use the (x3)
// type parameter multiple times if and only if they are returned. (x3)
// other kind of type references always count as multiple uses (x3)
// TS treats mapped types like `{[k in "a"]: T}` like `{a: T}`. (x3)
// They have properties, so we need to avoid double-counting. (x3)
// TS doesn't count mapped types key remapping (`{[K in 'a' as T]: K}`)
// but handles this under `MappedType.nameType`, so we need to visit that too.

Code
function collectTypeParameterUsageCounts(
  checker: ts.TypeChecker,
  node: ts.Node,
  foundIdentifierUsages: Map<ts.Identifier, number>,
  fromClass: boolean, // We are talking about the type parameters of a class or one of its methods
): void {
  const visitedSymbolLists = new Set<ts.Symbol[]>();
  const type = checker.getTypeAtLocation(node);
  const typeUsages = new Map<ts.Type, number>();
  const visitedConstraints = new Set<ts.TypeNode>();
  let functionLikeType = false;
  let visitedDefault = false;

  if (
    ts.isCallSignatureDeclaration(node) ||
    ts.isConstructorDeclaration(node)
  ) {
    functionLikeType = true;
    visitSignature(checker.getSignatureFromDeclaration(node));
  }

  if (!functionLikeType) {
    visitType(type, false);
  }

  function visitType(
    type: ts.Type | undefined,
    assumeMultipleUses: boolean,
    isReturnType = false,
  ): void {
    // Seeing the same type > (threshold=3 ** 2) times indicates a likely
    // recursive type, like `type T = { [P in keyof T]: T }`.
    // If it's not recursive, then heck, we've seen it enough times that any
    // referenced types have been counted enough to qualify as used.
    if (!type || incrementTypeUsages(type) > 9) {
      return;
    }

    if (tsutils.isTypeParameter(type)) {
      const declaration = type.getSymbol()?.getDeclarations()?.[0] as
        ts.TypeParameterDeclaration | undefined;

      if (declaration) {
        incrementIdentifierCount(declaration.name, assumeMultipleUses);

        // Visiting the type of a constrained type parameter will recurse into
        // the constraint. We avoid infinite loops by visiting each only once.
        if (
          declaration.constraint &&
          !visitedConstraints.has(declaration.constraint)
        ) {
          visitedConstraints.add(declaration.constraint);
          visitType(checker.getTypeAtLocation(declaration.constraint), false);
        }

        if (declaration.default && !visitedDefault) {
          visitedDefault = true;
          visitType(checker.getTypeAtLocation(declaration.default), false);
        }
      }
    }

    // Catch-all: generic type references like `Exclude<T, null>`
    else if (type.aliasTypeArguments) {
      // We don't descend into the definition of the type alias, so we don't
      // know whether it's used multiple times. It's safest to assume it is.
      visitTypesList(type.aliasTypeArguments, true);
    }

    // Intersections and unions like `0 | 1`
    else if (tsutils.isUnionOrIntersectionType(type)) {
      visitTypesList(type.types, assumeMultipleUses);
    }

    // Index access types like `T[K]`
    else if (tsutils.isIndexedAccessType(type)) {
      visitType(type.objectType, assumeMultipleUses);
      visitType(type.indexType, assumeMultipleUses);
    }

    // Tuple types like `[K, V]`
    // Generic type references like `Map<K, V>`
    else if (tsutils.isTypeReference(type)) {
      for (const typeArgument of type.typeArguments ?? []) {
        // currently, if we are in a "class context", everything is accepted
        let thisAssumeMultipleUses = fromClass || assumeMultipleUses;

        // special cases - readonly arrays/tuples are considered only to use the
        // type parameter once. Mutable arrays/tuples are considered to use the
        // type parameter multiple times if and only if they are returned.
        // other kind of type references always count as multiple uses
        thisAssumeMultipleUses ||= tsutils.isTupleType(type.target)
          ? isReturnType && !type.target.readonly
          : checker.isArrayType(type.target)
            ? isReturnType &&
              (type.symbol as ts.Symbol | undefined)?.getName() === 'Array'
            : true;

        visitType(typeArgument, thisAssumeMultipleUses, isReturnType);
      }
    }

    // Template literals like `a${T}b`
    else if (tsutils.isTemplateLiteralType(type)) {
      for (const subType of type.types) {
        visitType(subType, assumeMultipleUses);
      }
    }

    // Conditional types like `T extends string ? T : never`
    else if (tsutils.isConditionalType(type)) {
      visitType(type.checkType, assumeMultipleUses);
      visitType(type.extendsType, assumeMultipleUses);
    }

    // Catch-all: inferred object types like `{ K: V }`.
    // These catch-alls should be _after_ more specific checks like
    // `isTypeReference` to avoid descending into all the properties of a
    // generic interface/class, e.g. `Map<K, V>`.
    else if (tsutils.isObjectType(type)) {
      const properties = type.getProperties();
      visitSymbolsListOnce(properties, false);

      if (isMappedType(type)) {
        visitType(type.typeParameter, false);
        if (properties.length === 0) {
          // TS treats mapped types like `{[k in "a"]: T}` like `{a: T}`.
          // They have properties, so we need to avoid double-counting.
          visitType(type.templateType ?? type.constraintType, false);
        }

        // TS doesn't count mapped types key remapping (`{[K in 'a' as T]: K}`)
        // but handles this under `MappedType.nameType`, so we need to visit that too.
        if (type.nameType) {
          visitType(type.nameType, false);
        }
      }

      visitType(type.getNumberIndexType(), true);
      visitType(type.getStringIndexType(), true);

      type.getCallSignatures().forEach(signature => {
        functionLikeType = true;
        visitSignature(signature);
      });

      type.getConstructSignatures().forEach(signature => {
        functionLikeType = true;
        visitSignature(signature);
      });
    }

    // Catch-all: operator types like `keyof T`
    else if (isOperatorType(type)) {
      visitType(type.type, assumeMultipleUses);
    }
  }

  function incrementIdentifierCount(
    id: ts.Identifier,
    assumeMultipleUses: boolean,
  ): void {
    const identifierCount = foundIdentifierUsages.get(id) ?? 0;
    const value = assumeMultipleUses ? 2 : 1;
    foundIdentifierUsages.set(id, identifierCount + value);
  }

  function incrementTypeUsages(type: ts.Type): number {
    const count = (typeUsages.get(type) ?? 0) + 1;
    typeUsages.set(type, count);
    return count;
  }

  function visitSignature(signature: ts.Signature | undefined): void {
    if (!signature) {
      return;
    }

    if (signature.thisParameter) {
      visitType(checker.getTypeOfSymbol(signature.thisParameter), false);
    }

    for (const parameter of signature.parameters) {
      visitType(checker.getTypeOfSymbol(parameter), false);
    }

    for (const typeParameter of signature.getTypeParameters() ?? []) {
      visitType(typeParameter, false);
    }

    visitType(
      checker.getTypePredicateOfSignature(signature)?.type ??
        signature.getReturnType(),
      false,
      true,
    );
  }

  function visitSymbolsListOnce(
    symbols: ts.Symbol[],
    assumeMultipleUses: boolean,
  ): void {
    if (visitedSymbolLists.has(symbols)) {
      return;
    }

    visitedSymbolLists.add(symbols);

    for (const symbol of symbols) {
      visitType(checker.getTypeOfSymbol(symbol), assumeMultipleUses);
    }
  }

  function visitTypesList(
    types: readonly ts.Type[],
    assumeMultipleUses: boolean,
  ): void {
    for (const type of types) {
      visitType(type, assumeMultipleUses);
    }
  }
}

isMappedType(type: ts.Type): type is MappedType

Parameters:

  • type ts.Type

Returns: type is MappedType

Code
function isMappedType(type: ts.Type): type is MappedType {
  return 'typeParameter' in type;
}

isOperatorType(type: ts.Type): type is OperatorType

Parameters:

  • type ts.Type

Returns: type is OperatorType

Code
function isOperatorType(type: ts.Type): type is OperatorType {
  return 'type' in type && !!type.type;
}

Internal helpers

Declared inside another function in this file.

checkNode(node: TSESTree.FunctionLike, descriptor: string): void

Parameters:

  • node TSESTree.FunctionLike
  • descriptor string

Returns: void

Calls:

  • parserServices.esTreeNodeToTSNodeMap.get
  • parserServices.program.getTypeChecker
  • context.sourceCode.getScope
  • parserServices.tsNodeToESTreeNodeMap.get
  • nullThrows (from ../util)
  • complex_call_1882
  • scope.set.get
  • isTypeParameterRepeatedInAST
  • countTypeParameterUsage
  • counts.get
  • context.report
  • context.sourceCode.getText
  • [ AST_NODE_TYPES.TSArrayType, AST_NODE_TYPES.TSIndexedAccessType, AST_NODE_TYPES.TSIntersectionType, AST_NODE_TYPES.TSUnionType, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ].some
  • complex_call_4700
  • fixer.replaceText
  • fixer.remove
  • typeParamsNode.params.indexOf
  • context.sourceCode.getTokenAfter
  • NullThrowsReasons.MissingToken
  • fixer.removeRange
  • context.sourceCode.getTokenBefore

Internal Comments:

// Get the scope in which the type parameters are declared. (x2)
// Quick path: if the type parameter is used multiple times in the AST,
// we don't need to dip into types to know it's repeated.
// For any inferred types, we have to dip into type checking. (x3)
// Replace all the usages of the type parameter with the constraint... (x2)
// special case - a constraint of 'any' actually acts like 'unknown' (x2)
// ...and remove the type parameter itself from the declaration. (x2)
// We are assuming at this point that the reported type parameter
// is present in the inspected node's type parameters.
// Remove the whole <T> generic syntax if we're removing the only type parameter in the list. (x2)

Code
function checkNode(node: TSESTree.FunctionLike, descriptor: string): void {
      const tsNode = parserServices.esTreeNodeToTSNodeMap.get(
        node,
      ) as NodeWithTypeParameters;

      const checker = parserServices.program.getTypeChecker();
      let counts: Map<ts.Identifier, number> | undefined;

      // Get the scope in which the type parameters are declared.
      const scope = context.sourceCode.getScope(node);

      for (const typeParameter of tsNode.typeParameters) {
        const esTypeParameter =
          parserServices.tsNodeToESTreeNodeMap.get<TSESTree.TSTypeParameter>(
            typeParameter,
          );

        const smTypeParameterVariable = nullThrows(
          (() => {
            const variable = scope.set.get(esTypeParameter.name.name);
            return variable?.isTypeVariable ? variable : undefined;
          })(),
          "Type parameter should be present in scope's variables.",
        );

        // Quick path: if the type parameter is used multiple times in the AST,
        // we don't need to dip into types to know it's repeated.
        if (
          isTypeParameterRepeatedInAST(
            esTypeParameter,
            smTypeParameterVariable.references,
            node.body?.range[0] ?? node.returnType?.range[1],
          )
        ) {
          continue;
        }

        // For any inferred types, we have to dip into type checking.
        counts ??= countTypeParameterUsage(checker, tsNode);
        const identifierCounts = counts.get(typeParameter.name);
        if (!identifierCounts || identifierCounts > 2) {
          continue;
        }

        context.report({
          node: esTypeParameter,
          messageId: 'sole',
          data: {
            name: typeParameter.name.text,
            descriptor,
            uses: identifierCounts === 1 ? 'never used' : 'used only once',
          },
          suggest: [
            {
              messageId: 'replaceUsagesWithConstraint',
              *fix(fixer): Generator<TSESLint.RuleFix> {
                // Replace all the usages of the type parameter with the constraint...

                const constraint = esTypeParameter.constraint;
                // special case - a constraint of 'any' actually acts like 'unknown'
                const constraintText =
                  constraint != null &&
                  constraint.type !== AST_NODE_TYPES.TSAnyKeyword
                    ? context.sourceCode.getText(constraint)
                    : 'unknown';
                for (const reference of smTypeParameterVariable.references) {
                  if (reference.isTypeReference) {
                    const referenceNode = reference.identifier;
                    const isComplexType =
                      constraint?.type === AST_NODE_TYPES.TSUnionType ||
                      constraint?.type === AST_NODE_TYPES.TSIntersectionType ||
                      constraint?.type === AST_NODE_TYPES.TSConditionalType;
                    const hasMatchingAncestorType = [
                      AST_NODE_TYPES.TSArrayType,
                      AST_NODE_TYPES.TSIndexedAccessType,
                      AST_NODE_TYPES.TSIntersectionType,
                      AST_NODE_TYPES.TSUnionType,
                      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    ].some(type => referenceNode.parent.parent!.type === type);
                    if (isComplexType && hasMatchingAncestorType) {
                      const fixResult = getWrappingFixer({
                        node: referenceNode,
                        innerNode: constraint,
                        sourceCode: context.sourceCode,
                        wrap: constraintNode => constraintNode,
                      })(fixer);
                      yield fixResult;
                    } else {
                      yield fixer.replaceText(referenceNode, constraintText);
                    }
                  }
                }

                // ...and remove the type parameter itself from the declaration.

                const typeParamsNode = nullThrows(
                  node.typeParameters,
                  'node should have type parameters',
                );

                // We are assuming at this point that the reported type parameter
                // is present in the inspected node's type parameters.
                if (typeParamsNode.params.length === 1) {
                  // Remove the whole <T> generic syntax if we're removing the only type parameter in the list.
                  yield fixer.remove(typeParamsNode);
                } else {
                  const index = typeParamsNode.params.indexOf(esTypeParameter);

                  if (index === 0) {
                    const commaAfter = nullThrows(
                      context.sourceCode.getTokenAfter(
                        esTypeParameter,
                        token => token.value === ',',
                      ),
                      NullThrowsReasons.MissingToken(
                        'comma',
                        'type parameter list',
                      ),
                    );

                    const tokenAfterComma = nullThrows(
                      context.sourceCode.getTokenAfter(commaAfter, {
                        includeComments: true,
                      }),
                      NullThrowsReasons.MissingToken(
                        'token',
                        'type parameter list',
                      ),
                    );

                    yield fixer.removeRange([
                      esTypeParameter.range[0],
                      tokenAfterComma.range[0],
                    ]);
                  } else {
                    const commaBefore = nullThrows(
                      context.sourceCode.getTokenBefore(
                        esTypeParameter,
                        token => token.value === ',',
                      ),
                      NullThrowsReasons.MissingToken(
                        'comma',
                        'type parameter list',
                      ),
                    );

                    yield fixer.removeRange([
                      commaBefore.range[0],
                      esTypeParameter.range[1],
                    ]);
                  }
                }
              },
            },
          ],
        });
      }
    }

visitType(type: ts.Type | undefined, assumeMultipleUses: boolean, isReturnType: boolean): void

Parameters:

  • type ts.Type | undefined
  • assumeMultipleUses boolean
  • isReturnType boolean

Returns: void

Calls:

  • incrementTypeUsages
  • tsutils.isTypeParameter
  • type.getSymbol()?.getDeclarations
  • incrementIdentifierCount
  • visitedConstraints.has
  • visitedConstraints.add
  • visitType
  • checker.getTypeAtLocation
  • visitTypesList
  • tsutils.isUnionOrIntersectionType
  • tsutils.isIndexedAccessType
  • tsutils.isTypeReference
  • tsutils.isTupleType
  • checker.isArrayType
  • (type.symbol as ts.Symbol | undefined)?.getName
  • tsutils.isTemplateLiteralType
  • tsutils.isConditionalType
  • tsutils.isObjectType
  • type.getProperties
  • visitSymbolsListOnce
  • isMappedType
  • type.getNumberIndexType
  • type.getStringIndexType
  • type.getCallSignatures().forEach
  • visitSignature
  • type.getConstructSignatures().forEach
  • isOperatorType

Internal Comments:

// Seeing the same type > (threshold=3 ** 2) times indicates a likely
// recursive type, like `type T = { [P in keyof T]: T }`.
// If it's not recursive, then heck, we've seen it enough times that any
// referenced types have been counted enough to qualify as used.
// Visiting the type of a constrained type parameter will recurse into
// the constraint. We avoid infinite loops by visiting each only once.
// We don't descend into the definition of the type alias, so we don't (x3)
// know whether it's used multiple times. It's safest to assume it is. (x3)
// currently, if we are in a "class context", everything is accepted (x2)
// special cases - readonly arrays/tuples are considered only to use the (x3)
// type parameter once. Mutable arrays/tuples are considered to use the (x3)
// type parameter multiple times if and only if they are returned. (x3)
// other kind of type references always count as multiple uses (x3)
// TS treats mapped types like `{[k in "a"]: T}` like `{a: T}`. (x3)
// They have properties, so we need to avoid double-counting. (x3)
// TS doesn't count mapped types key remapping (`{[K in 'a' as T]: K}`)
// but handles this under `MappedType.nameType`, so we need to visit that too.

Code
function visitType(
    type: ts.Type | undefined,
    assumeMultipleUses: boolean,
    isReturnType = false,
  ): void {
    // Seeing the same type > (threshold=3 ** 2) times indicates a likely
    // recursive type, like `type T = { [P in keyof T]: T }`.
    // If it's not recursive, then heck, we've seen it enough times that any
    // referenced types have been counted enough to qualify as used.
    if (!type || incrementTypeUsages(type) > 9) {
      return;
    }

    if (tsutils.isTypeParameter(type)) {
      const declaration = type.getSymbol()?.getDeclarations()?.[0] as
        ts.TypeParameterDeclaration | undefined;

      if (declaration) {
        incrementIdentifierCount(declaration.name, assumeMultipleUses);

        // Visiting the type of a constrained type parameter will recurse into
        // the constraint. We avoid infinite loops by visiting each only once.
        if (
          declaration.constraint &&
          !visitedConstraints.has(declaration.constraint)
        ) {
          visitedConstraints.add(declaration.constraint);
          visitType(checker.getTypeAtLocation(declaration.constraint), false);
        }

        if (declaration.default && !visitedDefault) {
          visitedDefault = true;
          visitType(checker.getTypeAtLocation(declaration.default), false);
        }
      }
    }

    // Catch-all: generic type references like `Exclude<T, null>`
    else if (type.aliasTypeArguments) {
      // We don't descend into the definition of the type alias, so we don't
      // know whether it's used multiple times. It's safest to assume it is.
      visitTypesList(type.aliasTypeArguments, true);
    }

    // Intersections and unions like `0 | 1`
    else if (tsutils.isUnionOrIntersectionType(type)) {
      visitTypesList(type.types, assumeMultipleUses);
    }

    // Index access types like `T[K]`
    else if (tsutils.isIndexedAccessType(type)) {
      visitType(type.objectType, assumeMultipleUses);
      visitType(type.indexType, assumeMultipleUses);
    }

    // Tuple types like `[K, V]`
    // Generic type references like `Map<K, V>`
    else if (tsutils.isTypeReference(type)) {
      for (const typeArgument of type.typeArguments ?? []) {
        // currently, if we are in a "class context", everything is accepted
        let thisAssumeMultipleUses = fromClass || assumeMultipleUses;

        // special cases - readonly arrays/tuples are considered only to use the
        // type parameter once. Mutable arrays/tuples are considered to use the
        // type parameter multiple times if and only if they are returned.
        // other kind of type references always count as multiple uses
        thisAssumeMultipleUses ||= tsutils.isTupleType(type.target)
          ? isReturnType && !type.target.readonly
          : checker.isArrayType(type.target)
            ? isReturnType &&
              (type.symbol as ts.Symbol | undefined)?.getName() === 'Array'
            : true;

        visitType(typeArgument, thisAssumeMultipleUses, isReturnType);
      }
    }

    // Template literals like `a${T}b`
    else if (tsutils.isTemplateLiteralType(type)) {
      for (const subType of type.types) {
        visitType(subType, assumeMultipleUses);
      }
    }

    // Conditional types like `T extends string ? T : never`
    else if (tsutils.isConditionalType(type)) {
      visitType(type.checkType, assumeMultipleUses);
      visitType(type.extendsType, assumeMultipleUses);
    }

    // Catch-all: inferred object types like `{ K: V }`.
    // These catch-alls should be _after_ more specific checks like
    // `isTypeReference` to avoid descending into all the properties of a
    // generic interface/class, e.g. `Map<K, V>`.
    else if (tsutils.isObjectType(type)) {
      const properties = type.getProperties();
      visitSymbolsListOnce(properties, false);

      if (isMappedType(type)) {
        visitType(type.typeParameter, false);
        if (properties.length === 0) {
          // TS treats mapped types like `{[k in "a"]: T}` like `{a: T}`.
          // They have properties, so we need to avoid double-counting.
          visitType(type.templateType ?? type.constraintType, false);
        }

        // TS doesn't count mapped types key remapping (`{[K in 'a' as T]: K}`)
        // but handles this under `MappedType.nameType`, so we need to visit that too.
        if (type.nameType) {
          visitType(type.nameType, false);
        }
      }

      visitType(type.getNumberIndexType(), true);
      visitType(type.getStringIndexType(), true);

      type.getCallSignatures().forEach(signature => {
        functionLikeType = true;
        visitSignature(signature);
      });

      type.getConstructSignatures().forEach(signature => {
        functionLikeType = true;
        visitSignature(signature);
      });
    }

    // Catch-all: operator types like `keyof T`
    else if (isOperatorType(type)) {
      visitType(type.type, assumeMultipleUses);
    }
  }

incrementIdentifierCount(id: ts.Identifier, assumeMultipleUses: boolean): void

Parameters:

  • id ts.Identifier
  • assumeMultipleUses boolean

Returns: void

Calls:

  • foundIdentifierUsages.get
  • foundIdentifierUsages.set
Code
function incrementIdentifierCount(
    id: ts.Identifier,
    assumeMultipleUses: boolean,
  ): void {
    const identifierCount = foundIdentifierUsages.get(id) ?? 0;
    const value = assumeMultipleUses ? 2 : 1;
    foundIdentifierUsages.set(id, identifierCount + value);
  }

incrementTypeUsages(type: ts.Type): number

Parameters:

  • type ts.Type

Returns: number

Calls:

  • typeUsages.get
  • typeUsages.set
Code
function incrementTypeUsages(type: ts.Type): number {
    const count = (typeUsages.get(type) ?? 0) + 1;
    typeUsages.set(type, count);
    return count;
  }

visitSignature(signature: ts.Signature | undefined): void

Parameters:

  • signature ts.Signature | undefined

Returns: void

Calls:

  • visitType
  • checker.getTypeOfSymbol
  • signature.getTypeParameters
  • checker.getTypePredicateOfSignature
  • signature.getReturnType
Code
function visitSignature(signature: ts.Signature | undefined): void {
    if (!signature) {
      return;
    }

    if (signature.thisParameter) {
      visitType(checker.getTypeOfSymbol(signature.thisParameter), false);
    }

    for (const parameter of signature.parameters) {
      visitType(checker.getTypeOfSymbol(parameter), false);
    }

    for (const typeParameter of signature.getTypeParameters() ?? []) {
      visitType(typeParameter, false);
    }

    visitType(
      checker.getTypePredicateOfSignature(signature)?.type ??
        signature.getReturnType(),
      false,
      true,
    );
  }

visitSymbolsListOnce(symbols: ts.Symbol[], assumeMultipleUses: boolean): void

Parameters:

  • symbols ts.Symbol[]
  • assumeMultipleUses boolean

Returns: void

Calls:

  • visitedSymbolLists.has
  • visitedSymbolLists.add
  • visitType
  • checker.getTypeOfSymbol
Code
function visitSymbolsListOnce(
    symbols: ts.Symbol[],
    assumeMultipleUses: boolean,
  ): void {
    if (visitedSymbolLists.has(symbols)) {
      return;
    }

    visitedSymbolLists.add(symbols);

    for (const symbol of symbols) {
      visitType(checker.getTypeOfSymbol(symbol), assumeMultipleUses);
    }
  }

visitTypesList(types: readonly ts.Type[], assumeMultipleUses: boolean): void

Parameters:

  • types readonly ts.Type[]
  • assumeMultipleUses boolean

Returns: void

Calls:

  • visitType
Code
function visitTypesList(
    types: readonly ts.Type[],
    assumeMultipleUses: boolean,
  ): void {
    for (const type of types) {
      visitType(type, assumeMultipleUses);
    }
  }

Interfaces

MappedType

Interface Code
interface MappedType extends ts.ObjectType {
  constraintType?: ts.Type;
  templateType?: ts.Type;
  typeParameter: ts.Type;
  nameType?: ts.Type;
}

Properties

Name Type Optional Description
constraintType ts.Type not shown
templateType ts.Type not shown
typeParameter ts.Type not shown
nameType ts.Type not shown

OperatorType

Interface Code
interface OperatorType extends ts.Type {
  type: ts.Type;
}

Properties

Name Type Optional Description
type ts.Type not shown

Type Aliases

NodeWithTypeParameters

type NodeWithTypeParameters = MakeRequired<
  ts.ClassLikeDeclaration | ts.SignatureDeclaration,
  'typeParameters'
>;

Generated by Syntax Scribe