Skip to content

⬅️ Back to Table of Contents

πŸ“„ prefer-function-type

πŸ“Š Analysis Summary

Metric Count
πŸ”§ Functions 4
πŸ“¦ Imports 5
πŸ“Š Variables & Constants 1

πŸ“š Table of Contents

πŸ› οΈ File Location:

πŸ“‚ packages/eslint-plugin/src/rules/prefer-function-type.ts

πŸ“€ Default Export

export default createRule({ ... })
Property Value
name 'prefer-function-type'
meta.type 'suggestion'
meta.docs.description 'Enforce using function types instead of interfaces with call signatures'
meta.docs.recommended 'stylistic'
meta.fixable 'code'
meta.messages.functionTypeOverCallableType '{{ literalOrInterface }} only has a call signature, you should use a function type instead.'
meta.messages.unexpectedThisOnFunctionOnlyInterface "this refers to the function type '{{ interfaceName }}', did you intend to use a generic this parameter like `<Se...
meta.schema []
defaultOptions []

Entry point: create β€” documented under Functions.


πŸ“¦ Imports

Name Source
TSESLint @typescript-eslint/utils
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
AST_TOKEN_TYPES @typescript-eslint/utils
createRule ../util

Variables & Constants

Name Type Kind Value Exported
phrases { readonly [x: number]: "Interface" \... const { [AST_NODE_TYPES.TSInterfaceDeclaration]: 'Interface', [AST_NODE_TYPES.TSTyp... βœ“

Functions

create(context: any): { TSInterfaceDeclaration(): void; 'TSInterfaceDeclaration:e…

Parameters:

  • context any

Returns: { TSInterfaceDeclaration(): void; 'TSInterfaceDeclaration:exit'(node: TSESTree.TSInterfaceDeclaration): void; 'TSInterfaceDeclaration TSThisType'(node: TSESTree.TSThisType): void; 'TSInterfaceDeclaration TSTypeLiteral'(): void; 'TSInterfaceDeclaration TSTypeLiteral:exit'(): void; 'TSTypeLiteral[members.length = 1]'(node: TSESTree.TSTypeLiteral): void; }

Calls:

  • context.report
  • context.sourceCode .getText() .slice
  • context.sourceCode.getCommentsBefore
  • context.sourceCode.getCommentsAfter
  • text.slice
  • suggestion.endsWith
  • suggestion.slice
  • shouldWrapSuggestion
  • context.sourceCode .getText() .slice
  • comments .map(({ type, value }) => type === AST_TOKEN_TYPES.Line ?//${value}\n:/${value}/\n, ) .join
  • fixes.push
  • fixer.insertTextBefore
  • comments.forEach
  • fixer.replaceTextRange
  • hasOneSupertype
  • checkMember
  • tsThisTypes.push

Internal Comments:

/**
     * Checks if there the interface has exactly one supertype that isn't named 'Function'
     * @param node The node being checked
     */
/**
     * @param parent The parent of the call signature causing the diagnostic
     */
/**
     * @param member The TypeElement being checked
     * @param node The parent of member being checked
     */
// the message can be confusing if we don't point directly to the `this` node instead of the whole member (x4)
// and in favour of generating at most one error we'll only report the first occurrence of `this` if there are multiple (x4)
// https://github.com/microsoft/TypeScript/pull/56908 (x2)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion (x2)
// comments should move before export and not between export and interface declaration (x4)
// when entering an interface reset the count of `this`s to empty. (x3)
// on exit check member and reset the array to nothing. (x3)
// inside an interface keep track of all ThisType references.
// unless it's inside a nested type literal in which case it's invalid code anyway
// we don't want to incorrectly say "it refers to name" while typescript says it's completely invalid.
// keep track of nested literals to avoid complaining about invalid `this` uses (x2)

Code
create(context) {
    /**
     * Checks if there the interface has exactly one supertype that isn't named 'Function'
     * @param node The node being checked
     */
    function hasOneSupertype(node: TSESTree.TSInterfaceDeclaration): boolean {
      if (node.extends.length === 0) {
        return false;
      }
      if (node.extends.length !== 1) {
        return true;
      }
      const expr = node.extends[0].expression;

      return (
        expr.type !== AST_NODE_TYPES.Identifier || expr.name !== 'Function'
      );
    }

    /**
     * @param parent The parent of the call signature causing the diagnostic
     */
    function shouldWrapSuggestion(parent: TSESTree.Node | undefined): boolean {
      if (!parent) {
        return false;
      }

      switch (parent.type) {
        case AST_NODE_TYPES.TSUnionType:
        case AST_NODE_TYPES.TSIntersectionType:
        case AST_NODE_TYPES.TSArrayType:
          return true;
        default:
          return false;
      }
    }

    /**
     * @param member The TypeElement being checked
     * @param node The parent of member being checked
     */
    function checkMember(
      member: TSESTree.TypeElement,
      node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeLiteral,
      tsThisTypes: TSESTree.TSThisType[] | null = null,
    ): void {
      if (
        (member.type === AST_NODE_TYPES.TSCallSignatureDeclaration ||
          member.type === AST_NODE_TYPES.TSConstructSignatureDeclaration) &&
        member.returnType != null
      ) {
        if (
          tsThisTypes?.length &&
          node.type === AST_NODE_TYPES.TSInterfaceDeclaration
        ) {
          // the message can be confusing if we don't point directly to the `this` node instead of the whole member
          // and in favour of generating at most one error we'll only report the first occurrence of `this` if there are multiple
          context.report({
            node: tsThisTypes[0],
            messageId: 'unexpectedThisOnFunctionOnlyInterface',
            data: {
              interfaceName: node.id.name,
            },
          });
          return;
        }

        const fixable =
          node.parent.type === AST_NODE_TYPES.ExportDefaultDeclaration;

        const fix = fixable
          ? null
          : (fixer: TSESLint.RuleFixer): TSESLint.RuleFix[] => {
              const fixes: TSESLint.RuleFix[] = [];
              const start = member.range[0];
              // https://github.com/microsoft/TypeScript/pull/56908
              // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
              const colonPos = member.returnType!.range[0] - start;
              const text = context.sourceCode
                .getText()
                .slice(start, member.range[1]);
              const comments = [
                ...context.sourceCode.getCommentsBefore(member),
                ...context.sourceCode.getCommentsAfter(member),
              ];
              let suggestion = `${text.slice(0, colonPos)} =>${text.slice(
                colonPos + 1,
              )}`;
              const lastChar = suggestion.endsWith(';') ? ';' : '';
              if (lastChar) {
                suggestion = suggestion.slice(0, -1);
              }
              if (shouldWrapSuggestion(node.parent)) {
                suggestion = `(${suggestion})`;
              }

              if (node.type === AST_NODE_TYPES.TSInterfaceDeclaration) {
                if (node.typeParameters != null) {
                  suggestion = `type ${context.sourceCode
                    .getText()
                    .slice(
                      node.id.range[0],
                      node.typeParameters.range[1],
                    )} = ${suggestion}${lastChar}`;
                } else {
                  suggestion = `type ${node.id.name} = ${suggestion}${lastChar}`;
                }
              }

              const isParentExported =
                node.parent.type === AST_NODE_TYPES.ExportNamedDeclaration;

              if (
                node.type === AST_NODE_TYPES.TSInterfaceDeclaration &&
                isParentExported
              ) {
                const commentsText = comments
                  .map(({ type, value }) =>
                    type === AST_TOKEN_TYPES.Line
                      ? `//${value}\n`
                      : `/*${value}*/\n`,
                  )
                  .join('');
                // comments should move before export and not between export and interface declaration
                fixes.push(fixer.insertTextBefore(node.parent, commentsText));
              } else {
                comments.forEach(comment => {
                  let commentText =
                    comment.type === AST_TOKEN_TYPES.Line
                      ? `//${comment.value}`
                      : `/*${comment.value}*/`;
                  const isCommentOnTheSameLine =
                    comment.loc.start.line === member.loc.start.line;
                  if (!isCommentOnTheSameLine) {
                    commentText += '\n';
                  } else {
                    commentText += ' ';
                  }
                  suggestion = commentText + suggestion;
                });
              }

              const fixStart = node.range[0];
              fixes.push(
                fixer.replaceTextRange([fixStart, node.range[1]], suggestion),
              );
              return fixes;
            };

        context.report({
          node: member,
          messageId: 'functionTypeOverCallableType',
          data: {
            literalOrInterface: phrases[node.type],
          },
          fix,
        });
      }
    }
    let tsThisTypes: TSESTree.TSThisType[] | null = null;
    let literalNesting = 0;
    return {
      TSInterfaceDeclaration(): void {
        // when entering an interface reset the count of `this`s to empty.
        tsThisTypes = [];
      },
      'TSInterfaceDeclaration:exit'(
        node: TSESTree.TSInterfaceDeclaration,
      ): void {
        if (!hasOneSupertype(node) && node.body.body.length === 1) {
          checkMember(node.body.body[0], node, tsThisTypes);
        }
        // on exit check member and reset the array to nothing.
        tsThisTypes = null;
      },
      'TSInterfaceDeclaration TSThisType'(node: TSESTree.TSThisType): void {
        // inside an interface keep track of all ThisType references.
        // unless it's inside a nested type literal in which case it's invalid code anyway
        // we don't want to incorrectly say "it refers to name" while typescript says it's completely invalid.
        if (literalNesting === 0 && tsThisTypes != null) {
          tsThisTypes.push(node);
        }
      },
      // keep track of nested literals to avoid complaining about invalid `this` uses
      'TSInterfaceDeclaration TSTypeLiteral'(): void {
        literalNesting += 1;
      },
      'TSInterfaceDeclaration TSTypeLiteral:exit'(): void {
        literalNesting -= 1;
      },
      'TSTypeLiteral[members.length = 1]'(node: TSESTree.TSTypeLiteral): void {
        checkMember(node.members[0], node);
      },
    };
  }

Internal helpers

Declared inside another function in this file.

hasOneSupertype(node: TSESTree.TSInterfaceDeclaration): boolean

Checks if there the interface has exactly one supertype that isn't named 'Function'

Parameters:

  • node any: The node being checked
Raw JSDoc
/**
     * Checks if there the interface has exactly one supertype that isn't named 'Function'
     * @param node The node being checked
     */
Code
function hasOneSupertype(node: TSESTree.TSInterfaceDeclaration): boolean {
      if (node.extends.length === 0) {
        return false;
      }
      if (node.extends.length !== 1) {
        return true;
      }
      const expr = node.extends[0].expression;

      return (
        expr.type !== AST_NODE_TYPES.Identifier || expr.name !== 'Function'
      );
    }

shouldWrapSuggestion(parent: TSESTree.Node | undefined): boolean

Parameters:

  • parent any: The parent of the call signature causing the diagnostic
Raw JSDoc
/**
     * @param parent The parent of the call signature causing the diagnostic
     */
Code
function shouldWrapSuggestion(parent: TSESTree.Node | undefined): boolean {
      if (!parent) {
        return false;
      }

      switch (parent.type) {
        case AST_NODE_TYPES.TSUnionType:
        case AST_NODE_TYPES.TSIntersectionType:
        case AST_NODE_TYPES.TSArrayType:
          return true;
        default:
          return false;
      }
    }

checkMember(member: TSESTree.TypeElement, node: TSESTree.TSInterfaceDeclaration | TSEST…, tsThisTypes: TSESTree.TSThisType[] | null): void

Parameters:

  • member any: The TypeElement being checked
  • node any: The parent of member being checked
Raw JSDoc
/**
     * @param member The TypeElement being checked
     * @param node The parent of member being checked
     */

Calls:

  • context.report
  • context.sourceCode .getText() .slice
  • context.sourceCode.getCommentsBefore
  • context.sourceCode.getCommentsAfter
  • text.slice
  • suggestion.endsWith
  • suggestion.slice
  • shouldWrapSuggestion
  • context.sourceCode .getText() .slice
  • comments .map(({ type, value }) => type === AST_TOKEN_TYPES.Line ?//${value}\n:/${value}/\n, ) .join
  • fixes.push
  • fixer.insertTextBefore
  • comments.forEach
  • fixer.replaceTextRange

Internal Comments:

// the message can be confusing if we don't point directly to the `this` node instead of the whole member (x4)
// and in favour of generating at most one error we'll only report the first occurrence of `this` if there are multiple (x4)
// https://github.com/microsoft/TypeScript/pull/56908 (x2)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion (x2)
// comments should move before export and not between export and interface declaration (x4)

Code
function checkMember(
      member: TSESTree.TypeElement,
      node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeLiteral,
      tsThisTypes: TSESTree.TSThisType[] | null = null,
    ): void {
      if (
        (member.type === AST_NODE_TYPES.TSCallSignatureDeclaration ||
          member.type === AST_NODE_TYPES.TSConstructSignatureDeclaration) &&
        member.returnType != null
      ) {
        if (
          tsThisTypes?.length &&
          node.type === AST_NODE_TYPES.TSInterfaceDeclaration
        ) {
          // the message can be confusing if we don't point directly to the `this` node instead of the whole member
          // and in favour of generating at most one error we'll only report the first occurrence of `this` if there are multiple
          context.report({
            node: tsThisTypes[0],
            messageId: 'unexpectedThisOnFunctionOnlyInterface',
            data: {
              interfaceName: node.id.name,
            },
          });
          return;
        }

        const fixable =
          node.parent.type === AST_NODE_TYPES.ExportDefaultDeclaration;

        const fix = fixable
          ? null
          : (fixer: TSESLint.RuleFixer): TSESLint.RuleFix[] => {
              const fixes: TSESLint.RuleFix[] = [];
              const start = member.range[0];
              // https://github.com/microsoft/TypeScript/pull/56908
              // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
              const colonPos = member.returnType!.range[0] - start;
              const text = context.sourceCode
                .getText()
                .slice(start, member.range[1]);
              const comments = [
                ...context.sourceCode.getCommentsBefore(member),
                ...context.sourceCode.getCommentsAfter(member),
              ];
              let suggestion = `${text.slice(0, colonPos)} =>${text.slice(
                colonPos + 1,
              )}`;
              const lastChar = suggestion.endsWith(';') ? ';' : '';
              if (lastChar) {
                suggestion = suggestion.slice(0, -1);
              }
              if (shouldWrapSuggestion(node.parent)) {
                suggestion = `(${suggestion})`;
              }

              if (node.type === AST_NODE_TYPES.TSInterfaceDeclaration) {
                if (node.typeParameters != null) {
                  suggestion = `type ${context.sourceCode
                    .getText()
                    .slice(
                      node.id.range[0],
                      node.typeParameters.range[1],
                    )} = ${suggestion}${lastChar}`;
                } else {
                  suggestion = `type ${node.id.name} = ${suggestion}${lastChar}`;
                }
              }

              const isParentExported =
                node.parent.type === AST_NODE_TYPES.ExportNamedDeclaration;

              if (
                node.type === AST_NODE_TYPES.TSInterfaceDeclaration &&
                isParentExported
              ) {
                const commentsText = comments
                  .map(({ type, value }) =>
                    type === AST_TOKEN_TYPES.Line
                      ? `//${value}\n`
                      : `/*${value}*/\n`,
                  )
                  .join('');
                // comments should move before export and not between export and interface declaration
                fixes.push(fixer.insertTextBefore(node.parent, commentsText));
              } else {
                comments.forEach(comment => {
                  let commentText =
                    comment.type === AST_TOKEN_TYPES.Line
                      ? `//${comment.value}`
                      : `/*${comment.value}*/`;
                  const isCommentOnTheSameLine =
                    comment.loc.start.line === member.loc.start.line;
                  if (!isCommentOnTheSameLine) {
                    commentText += '\n';
                  } else {
                    commentText += ' ';
                  }
                  suggestion = commentText + suggestion;
                });
              }

              const fixStart = node.range[0];
              fixes.push(
                fixer.replaceTextRange([fixStart, node.range[1]], suggestion),
              );
              return fixes;
            };

        context.report({
          node: member,
          messageId: 'functionTypeOverCallableType',
          data: {
            literalOrInterface: phrases[node.type],
          },
          fix,
        });
      }
    }

Generated by Syntax Scribe