Skip to content

⬅️ Back to Table of Contents

πŸ“„ no-duplicate-type-constituents

πŸ“Š Analysis Summary

Metric Count
πŸ”§ Functions 8
πŸ“¦ Imports 8
πŸ“Š Variables & Constants 1
πŸ“‘ Type Aliases 3

πŸ“š Table of Contents

πŸ› οΈ File Location:

πŸ“‚ packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts

πŸ“€ Default Export

export default createRule<Options, MessageIds>({ ... })
Property Value
name 'no-duplicate-type-constituents'
meta.type 'suggestion'
meta.docs.description 'Disallow duplicate constituents of union or intersection types'
meta.docs.recommended 'recommended'
meta.docs.requiresTypeChecking true
meta.fixable 'code'
meta.messages.duplicate '{{type}} type constituent is duplicated with {{previous}}.'
meta.messages.unnecessary 'Explicit undefined is unnecessary on an optional parameter.'
meta.schema [ { type: 'object', additionalProperties: false, properties: { ignoreIntersections: { type: 'boolean', description: '...
defaultOptions [ { ignoreIntersections: false, ignoreUnions: false, }, ]

Entry point: create β€” documented under Functions.


πŸ“¦ Imports

Name Source
TSESTree @typescript-eslint/utils
Type typescript
AST_NODE_TYPES @typescript-eslint/utils
createRule ../util
getParserServices ../util
isFunctionOrFunctionType ../util
nullThrows ../util
NullThrowsReasons ../util

Variables & Constants

Name Type Kind Value Exported
astIgnoreKeys Set<string> const new Set(['loc', 'parent', 'range']) βœ—

Functions

create(context: any, [{ ignoreIntersections, ignoreU…: any): { TSUnionType: (node: any) => void; TSIntersectionType(node…

Parameters:

  • context any
  • [{ ignoreIntersections, ignoreUnions }] any

Returns: { TSUnionType: (node: any) => void; TSIntersectionType(node: any): void; }

Calls:

  • getParserServices (from ../util)
  • sourceCode[getTokens${where}](constituentNode, { filter: token => ['&', '|'].includes(token.value) && constituentNode.parent.range[0] <= token.range[0] && token.range[1] <= constituentNode.parent.range[1], }).at
  • getUnionOrIntersectionToken
  • sourceCode.getTokensBetween
  • sourceCode.getTokensAfter
  • nullThrows (from ../util)
  • NullThrowsReasons.MissingToken
  • sourceCode.getTokensBefore
  • context.report
  • bracketAfterTokens.at
  • [ beforeUnionOrIntersectionToken, ...bracketBeforeTokens, constituentNode, ...bracketAfterTokens, afterUnionOrIntersectionToken, ].flatMap
  • fixer.remove
  • report
  • sourceCode.getText
  • uniqueConstituents.find
  • isSameAstNode
  • reportDuplicate
  • parserServices.getTypeAtLocation
  • tsutils.isIntrinsicErrorType
  • cachedTypeMap.get
  • forEachNodeType
  • cachedTypeMap.set
  • uniqueConstituents.push
  • checkDuplicateRecursively
  • checkDuplicate
  • isFunctionOrFunctionType (from ../util)
  • maybeFunction.params.includes
  • tsutils.isTypeFlagSet

Internal Comments:

// Check duplicates in the AST before type lookup for better performance. (x2)

Code
create(context, [{ ignoreIntersections, ignoreUnions }]) {
    const parserServices = getParserServices(context);
    const { sourceCode } = context;

    function report(
      messageId: MessageIds,
      constituentNode: TSESTree.TypeNode,
      data?: Record<string, unknown>,
    ): void {
      const getUnionOrIntersectionToken = (
        where: 'After' | 'Before',
        at: number,
      ): TSESTree.Token | undefined =>
        sourceCode[`getTokens${where}`](constituentNode, {
          filter: token =>
            ['&', '|'].includes(token.value) &&
            constituentNode.parent.range[0] <= token.range[0] &&
            token.range[1] <= constituentNode.parent.range[1],
        }).at(at);

      const beforeUnionOrIntersectionToken = getUnionOrIntersectionToken(
        'Before',
        -1,
      );
      let afterUnionOrIntersectionToken: TSESTree.Token | undefined;
      let bracketBeforeTokens;
      let bracketAfterTokens;
      if (beforeUnionOrIntersectionToken) {
        bracketBeforeTokens = sourceCode.getTokensBetween(
          beforeUnionOrIntersectionToken,
          constituentNode,
        );
        bracketAfterTokens = sourceCode.getTokensAfter(constituentNode, {
          count: bracketBeforeTokens.length,
        });
      } else {
        afterUnionOrIntersectionToken = nullThrows(
          getUnionOrIntersectionToken('After', 0),
          NullThrowsReasons.MissingToken(
            'union or intersection token',
            'duplicate type constituent',
          ),
        );
        bracketAfterTokens = sourceCode.getTokensBetween(
          constituentNode,
          afterUnionOrIntersectionToken,
        );
        bracketBeforeTokens = sourceCode.getTokensBefore(constituentNode, {
          count: bracketAfterTokens.length,
        });
      }
      context.report({
        loc: {
          start: constituentNode.loc.start,
          end: (bracketAfterTokens.at(-1) ?? constituentNode).loc.end,
        },
        node: constituentNode,
        messageId,
        data,
        fix: fixer =>
          [
            beforeUnionOrIntersectionToken,
            ...bracketBeforeTokens,
            constituentNode,
            ...bracketAfterTokens,
            afterUnionOrIntersectionToken,
          ].flatMap(token => (token ? fixer.remove(token) : [])),
      });
    }

    function checkDuplicateRecursively(
      unionOrIntersection: UnionOrIntersection,
      constituentNode: TSESTree.TypeNode,
      uniqueConstituents: TSESTree.TypeNode[],
      cachedTypeMap: Map<Type, TSESTree.TypeNode>,
      forEachNodeType?: (type: Type, node: TSESTree.TypeNode) => void,
    ): void {
      const reportDuplicate = (previous: TSESTree.TypeNode) => {
        report('duplicate', constituentNode, {
          type: unionOrIntersection,
          previous: sourceCode.getText(previous),
        });
      };

      // Check duplicates in the AST before type lookup for better performance.
      let duplicatedPrevious = uniqueConstituents.find(ele =>
        isSameAstNode(ele, constituentNode),
      );

      if (duplicatedPrevious) {
        reportDuplicate(duplicatedPrevious);
        return;
      }

      const type = parserServices.getTypeAtLocation(constituentNode);
      if (tsutils.isIntrinsicErrorType(type)) {
        return;
      }

      duplicatedPrevious = cachedTypeMap.get(type);

      if (duplicatedPrevious) {
        reportDuplicate(duplicatedPrevious);
        return;
      }

      forEachNodeType?.(type, constituentNode);
      cachedTypeMap.set(type, constituentNode);
      uniqueConstituents.push(constituentNode);

      if (
        (unionOrIntersection === 'Union' &&
          constituentNode.type === AST_NODE_TYPES.TSUnionType) ||
        (unionOrIntersection === 'Intersection' &&
          constituentNode.type === AST_NODE_TYPES.TSIntersectionType)
      ) {
        for (const constituent of constituentNode.types) {
          checkDuplicateRecursively(
            unionOrIntersection,
            constituent,
            uniqueConstituents,
            cachedTypeMap,
            forEachNodeType,
          );
        }
      }
    }

    function checkDuplicate(
      node: TSESTree.TSIntersectionType | TSESTree.TSUnionType,
      forEachNodeType?: (
        constituentNodeType: Type,
        constituentNode: TSESTree.TypeNode,
      ) => void,
    ): void {
      const cachedTypeMap = new Map<Type, TSESTree.TypeNode>();
      const uniqueConstituents: TSESTree.TypeNode[] = [];

      const unionOrIntersection =
        node.type === AST_NODE_TYPES.TSIntersectionType
          ? 'Intersection'
          : 'Union';

      for (const type of node.types) {
        checkDuplicateRecursively(
          unionOrIntersection,
          type,
          uniqueConstituents,
          cachedTypeMap,
          forEachNodeType,
        );
      }
    }

    return {
      ...(!ignoreIntersections && {
        TSIntersectionType(node) {
          if (node.parent.type === AST_NODE_TYPES.TSIntersectionType) {
            return;
          }
          checkDuplicate(node);
        },
      }),
      ...(!ignoreUnions && {
        TSUnionType: (node): void => {
          if (node.parent.type === AST_NODE_TYPES.TSUnionType) {
            return;
          }
          checkDuplicate(node, (constituentNodeType, constituentNode) => {
            const maybeTypeAnnotation = node.parent;
            if (maybeTypeAnnotation.type === AST_NODE_TYPES.TSTypeAnnotation) {
              const maybeIdentifier = maybeTypeAnnotation.parent;
              if (
                maybeIdentifier.type === AST_NODE_TYPES.Identifier &&
                maybeIdentifier.optional
              ) {
                const maybeFunction = maybeIdentifier.parent;
                if (
                  isFunctionOrFunctionType(maybeFunction) &&
                  maybeFunction.params.includes(maybeIdentifier) &&
                  tsutils.isTypeFlagSet(
                    constituentNodeType,
                    ts.TypeFlags.Undefined,
                  )
                ) {
                  report('unnecessary', constituentNode);
                }
              }
            }
          });
        },
      }),
    };
  }

isSameAstNode(actualNode: unknown, expectedNode: unknown): boolean

Parameters:

  • actualNode unknown
  • expectedNode unknown

Returns: boolean

Calls:

  • Array.isArray
  • actualNode.some
  • isSameAstNode
  • Object.keys(actualNode).filter
  • astIgnoreKeys.has
  • Object.keys(expectedNode).filter
  • actualNodeKeys.some
  • Object.hasOwn
Code
(actualNode: unknown, expectedNode: unknown): boolean => {
  if (actualNode === expectedNode) {
    return true;
  }
  if (
    actualNode &&
    expectedNode &&
    typeof actualNode === 'object' &&
    typeof expectedNode === 'object'
  ) {
    if (Array.isArray(actualNode) && Array.isArray(expectedNode)) {
      if (actualNode.length !== expectedNode.length) {
        return false;
      }
      return !actualNode.some(
        (nodeEle, index) => !isSameAstNode(nodeEle, expectedNode[index]),
      );
    }
    const actualNodeKeys = Object.keys(actualNode).filter(
      key => !astIgnoreKeys.has(key),
    );
    const expectedNodeKeys = Object.keys(expectedNode).filter(
      key => !astIgnoreKeys.has(key),
    );
    if (actualNodeKeys.length !== expectedNodeKeys.length) {
      return false;
    }
    if (
      actualNodeKeys.some(
        actualNodeKey => !Object.hasOwn(expectedNode, actualNodeKey),
      )
    ) {
      return false;
    }
    if (
      actualNodeKeys.some(
        actualNodeKey =>
          !isSameAstNode(
            actualNode[actualNodeKey as keyof typeof actualNode],
            expectedNode[actualNodeKey as keyof typeof expectedNode],
          ),
      )
    ) {
      return false;
    }
    return true;
  }
  return false;
}

Internal helpers

Declared inside another function in this file.

report(messageId: MessageIds, constituentNode: TSESTree.TypeNode, data: Record<string, unknown>): void

Parameters:

  • messageId MessageIds
  • constituentNode TSESTree.TypeNode
  • data Record<string, unknown>

Returns: void

Calls:

  • sourceCode[getTokens${where}](constituentNode, { filter: token => ['&', '|'].includes(token.value) && constituentNode.parent.range[0] <= token.range[0] && token.range[1] <= constituentNode.parent.range[1], }).at
  • getUnionOrIntersectionToken
  • sourceCode.getTokensBetween
  • sourceCode.getTokensAfter
  • nullThrows (from ../util)
  • NullThrowsReasons.MissingToken
  • sourceCode.getTokensBefore
  • context.report
  • bracketAfterTokens.at
  • [ beforeUnionOrIntersectionToken, ...bracketBeforeTokens, constituentNode, ...bracketAfterTokens, afterUnionOrIntersectionToken, ].flatMap
  • fixer.remove
Code
function report(
      messageId: MessageIds,
      constituentNode: TSESTree.TypeNode,
      data?: Record<string, unknown>,
    ): void {
      const getUnionOrIntersectionToken = (
        where: 'After' | 'Before',
        at: number,
      ): TSESTree.Token | undefined =>
        sourceCode[`getTokens${where}`](constituentNode, {
          filter: token =>
            ['&', '|'].includes(token.value) &&
            constituentNode.parent.range[0] <= token.range[0] &&
            token.range[1] <= constituentNode.parent.range[1],
        }).at(at);

      const beforeUnionOrIntersectionToken = getUnionOrIntersectionToken(
        'Before',
        -1,
      );
      let afterUnionOrIntersectionToken: TSESTree.Token | undefined;
      let bracketBeforeTokens;
      let bracketAfterTokens;
      if (beforeUnionOrIntersectionToken) {
        bracketBeforeTokens = sourceCode.getTokensBetween(
          beforeUnionOrIntersectionToken,
          constituentNode,
        );
        bracketAfterTokens = sourceCode.getTokensAfter(constituentNode, {
          count: bracketBeforeTokens.length,
        });
      } else {
        afterUnionOrIntersectionToken = nullThrows(
          getUnionOrIntersectionToken('After', 0),
          NullThrowsReasons.MissingToken(
            'union or intersection token',
            'duplicate type constituent',
          ),
        );
        bracketAfterTokens = sourceCode.getTokensBetween(
          constituentNode,
          afterUnionOrIntersectionToken,
        );
        bracketBeforeTokens = sourceCode.getTokensBefore(constituentNode, {
          count: bracketAfterTokens.length,
        });
      }
      context.report({
        loc: {
          start: constituentNode.loc.start,
          end: (bracketAfterTokens.at(-1) ?? constituentNode).loc.end,
        },
        node: constituentNode,
        messageId,
        data,
        fix: fixer =>
          [
            beforeUnionOrIntersectionToken,
            ...bracketBeforeTokens,
            constituentNode,
            ...bracketAfterTokens,
            afterUnionOrIntersectionToken,
          ].flatMap(token => (token ? fixer.remove(token) : [])),
      });
    }

getUnionOrIntersectionToken(where: 'After' | 'Before', at: number): TSESTree.Token | undefined

Parameters:

  • where 'After' | 'Before'
  • at number

Returns: TSESTree.Token | undefined

Calls:

  • sourceCode[getTokens${where}](constituentNode, { filter: token => ['&', '|'].includes(token.value) && constituentNode.parent.range[0] <= token.range[0] && token.range[1] <= constituentNode.parent.range[1], }).at
Code
(
        where: 'After' | 'Before',
        at: number,
      ): TSESTree.Token | undefined =>
        sourceCode[`getTokens${where}`](constituentNode, {
          filter: token =>
            ['&', '|'].includes(token.value) &&
            constituentNode.parent.range[0] <= token.range[0] &&
            token.range[1] <= constituentNode.parent.range[1],
        }).at(at)

checkDuplicateRecursively(…): void

Parameters:

  • unionOrIntersection UnionOrIntersection
  • constituentNode TSESTree.TypeNode
  • uniqueConstituents TSESTree.TypeNode[]
  • cachedTypeMap Map<Type, TSESTree.TypeNode>
  • forEachNodeType (type: Type, node: TSESTree.TypeNode) => void

Returns: void

Calls:

  • report
  • sourceCode.getText
  • uniqueConstituents.find
  • isSameAstNode
  • reportDuplicate
  • parserServices.getTypeAtLocation
  • tsutils.isIntrinsicErrorType
  • cachedTypeMap.get
  • forEachNodeType
  • cachedTypeMap.set
  • uniqueConstituents.push
  • checkDuplicateRecursively

Internal Comments:

// Check duplicates in the AST before type lookup for better performance. (x2)

Code
function checkDuplicateRecursively(
      unionOrIntersection: UnionOrIntersection,
      constituentNode: TSESTree.TypeNode,
      uniqueConstituents: TSESTree.TypeNode[],
      cachedTypeMap: Map<Type, TSESTree.TypeNode>,
      forEachNodeType?: (type: Type, node: TSESTree.TypeNode) => void,
    ): void {
      const reportDuplicate = (previous: TSESTree.TypeNode) => {
        report('duplicate', constituentNode, {
          type: unionOrIntersection,
          previous: sourceCode.getText(previous),
        });
      };

      // Check duplicates in the AST before type lookup for better performance.
      let duplicatedPrevious = uniqueConstituents.find(ele =>
        isSameAstNode(ele, constituentNode),
      );

      if (duplicatedPrevious) {
        reportDuplicate(duplicatedPrevious);
        return;
      }

      const type = parserServices.getTypeAtLocation(constituentNode);
      if (tsutils.isIntrinsicErrorType(type)) {
        return;
      }

      duplicatedPrevious = cachedTypeMap.get(type);

      if (duplicatedPrevious) {
        reportDuplicate(duplicatedPrevious);
        return;
      }

      forEachNodeType?.(type, constituentNode);
      cachedTypeMap.set(type, constituentNode);
      uniqueConstituents.push(constituentNode);

      if (
        (unionOrIntersection === 'Union' &&
          constituentNode.type === AST_NODE_TYPES.TSUnionType) ||
        (unionOrIntersection === 'Intersection' &&
          constituentNode.type === AST_NODE_TYPES.TSIntersectionType)
      ) {
        for (const constituent of constituentNode.types) {
          checkDuplicateRecursively(
            unionOrIntersection,
            constituent,
            uniqueConstituents,
            cachedTypeMap,
            forEachNodeType,
          );
        }
      }
    }

reportDuplicate(previous: TSESTree.TypeNode): void

Parameters:

  • previous TSESTree.TypeNode

Returns: void

Calls:

  • report
  • sourceCode.getText
Code
(previous: TSESTree.TypeNode) => {
        report('duplicate', constituentNode, {
          type: unionOrIntersection,
          previous: sourceCode.getText(previous),
        });
      }

checkDuplicate(node: TSESTree.TSIntersectionType | TSESTree.…, forEachNodeType: ( constituentNodeType: Type, constituen…): void

Parameters:

  • node TSESTree.TSIntersectionType | TSESTree.TSUnionType
  • forEachNodeType ( constituentNodeType: Type, constituentNode: TSESTree.TypeNode, ) => void

Returns: void

Calls:

  • checkDuplicateRecursively
Code
function checkDuplicate(
      node: TSESTree.TSIntersectionType | TSESTree.TSUnionType,
      forEachNodeType?: (
        constituentNodeType: Type,
        constituentNode: TSESTree.TypeNode,
      ) => void,
    ): void {
      const cachedTypeMap = new Map<Type, TSESTree.TypeNode>();
      const uniqueConstituents: TSESTree.TypeNode[] = [];

      const unionOrIntersection =
        node.type === AST_NODE_TYPES.TSIntersectionType
          ? 'Intersection'
          : 'Union';

      for (const type of node.types) {
        checkDuplicateRecursively(
          unionOrIntersection,
          type,
          uniqueConstituents,
          cachedTypeMap,
          forEachNodeType,
        );
      }
    }

TSUnionType(node: any): void

Parameters:

  • node any

Returns: void

Calls:

  • checkDuplicate
  • isFunctionOrFunctionType (from ../util)
  • maybeFunction.params.includes
  • tsutils.isTypeFlagSet
  • report
Code
(node): void => {
          if (node.parent.type === AST_NODE_TYPES.TSUnionType) {
            return;
          }
          checkDuplicate(node, (constituentNodeType, constituentNode) => {
            const maybeTypeAnnotation = node.parent;
            if (maybeTypeAnnotation.type === AST_NODE_TYPES.TSTypeAnnotation) {
              const maybeIdentifier = maybeTypeAnnotation.parent;
              if (
                maybeIdentifier.type === AST_NODE_TYPES.Identifier &&
                maybeIdentifier.optional
              ) {
                const maybeFunction = maybeIdentifier.parent;
                if (
                  isFunctionOrFunctionType(maybeFunction) &&
                  maybeFunction.params.includes(maybeIdentifier) &&
                  tsutils.isTypeFlagSet(
                    constituentNodeType,
                    ts.TypeFlags.Undefined,
                  )
                ) {
                  report('unnecessary', constituentNode);
                }
              }
            }
          });
        }

Type Aliases

Options

type Options = [
  {
    ignoreIntersections?: boolean;
    ignoreUnions?: boolean;
  },
];

MessageIds

type MessageIds = 'duplicate' | 'unnecessary';

UnionOrIntersection

type UnionOrIntersection = 'Intersection' | 'Union';

Generated by Syntax Scribe