Search code examples
typescripteslinttypescript-eslint

How to validate the type of a variable in a typescript-eslint custom rule


I am trying to write a custom typescript-eslint rule where i need to make sure that the callee's object of the node is of a specific Type which i'll just call MyType here. So i am using the typescript-eslint' CallExpression, as seen in the code below

Source code:

interface MyType {
   ...
}

const someVariable: MyType = ...;

custom typescript-eslint rule:

export const rule = createRule({
  name: 'some-name',
  meta: {
    type: 'problem',
    docs: {
      description: 'description',
      recommended: 'error',
    },
    schema: [],
    messages,
  },
  defaultOptions: [],
  create: (context) => {
    return {
      CallExpression(node: TSESTree.CallExpression) {
        const services = ESLintUtils.getParserServices(context);
        const checker = services.program.getTypeChecker();

        const callee = node.callee;
        const object = callee.type === 'MemberExpression' ? callee.object : null;

        if (object === null) {
          return;
        }

        const type = checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(object));

        const isMyType = (
          object.type === 'Identifier' &&
          (type as any)?.symbol?.escapedName === 'MyType'
        );

        if (isMyType) {
          [... do stuff here]
        }
      },
    };
  },
});

However, my problem is that the symbol or escapedName is undefined so it doesnt equal 'MyType'. Is there a (better) way to check if a callee's object is of a specific type?


Solution

  • As per comments, the problem can be solved purely in TS template literals without the need for an ESLint rule. The error will also be at compile time for invalid invocations. TS template literals allow TS to understand the construction of a statically analysable string.

    One restriction here is the first arg must be provided directly, i.e. it can't be constructed through a variable which is composed of other strings. But this would also be true with the Eslint way.

    type StringToTuple<S extends any, A extends any[] = []> =
      S extends `${string}{}${infer Rest}`
        ? StringToTuple<Rest, [...A, any]>
        : A;
    
    const format = <S extends string>(inputStr: S, ...args: StringToTuple<S>): string  => {
      return inputStr // Do actual logic here
    }
    
    const result1 = format('something {} something {}', 10, 'two');  // OK
    const result2 = format('something {} something {}', 'one', 'two', 'three');  // Compile error
    const result3 = format('something {} something {} something {}', 'one', 'two', 'three');  // OK