DEV Community

Jennie
Jennie

Posted on

Identifying code with Typescript compiler

Since Typescript gets popular and typed code is easier to be identified, I found it is a good idea sometimes to do code automation with Typescript compiler instead of basing on the AST.

For example, when I tried to find out all the React function components with AST, I may require help from tool like Babel as following simplified code shows:

import { transformSync, Visitor } from "@babel/core";
import { Identifier } from "@babel/types";

const funcs = [];

transformSync(content, {
  parserOpts: {
    plugins: ["jsx", "typescript", "classProperties", "optionalChaining"],
  },
  plugins: [
    () => ({
      visitor: {
        ExportDefaultDeclaration({ node: { declaration } }) {
          switch (declaration.type) {
            case "FunctionDeclaration":
            case "ArrowFuntionExpression":
              funcs.push("default");
              break;
          }
        },
        ExportNamedDeclaration({ node }) {
          const { declaration } = node;
          if (declaration) {
            switch (declaration.type) {
              case "FunctionDeclaration":
                if (declaration.id) {
                  funcs.push(declaration.id.name);
                }
                break;
              case "VariableDeclaration":
                declaration.declarations.forEach((d) => {
                  if (!d.id.name) return;
                  switch (d.init?.type) {
                    case "ArrowFunctionExpression":
                      funcs.push(d.id.name);
                      break;
                  }
                });
                break;
            }
          }
        },
      },
    }),
  ],
});
console.log(funcs);
Enter fullscreen mode Exit fullscreen mode

Here my little VSCode plugin Babel AST Explorer helped me saved a lot of time.

You may found the above Visitors could only recognize following code:

export default function f() {}
Enter fullscreen mode Exit fullscreen mode
export default () => {};
Enter fullscreen mode Exit fullscreen mode
export function f() {}
Enter fullscreen mode Exit fullscreen mode
export const f = () => {};
Enter fullscreen mode Exit fullscreen mode

When they are assigned to variables, it gets even harder to identify. For instance:

function f() {}
export { f };
export const f2 = f;
export default f;
Enter fullscreen mode Exit fullscreen mode

This brought me to the Typescript compiler.

However, the document of Typescript compiler is kind of unhelpful. So here are some notes of the usage of Typescript compiler that I tested in a small project. Hopefully, it could save you some time.

The basic definitions

The Typescript compiler defines several basic objects, and I often worked with these 3: Node, Symbol, Type.

In my understanding,

  • Symbol is the major data structure that Typescript use to communicate in the compiler.
  • Node is the "superset" of AST node, which handles the token, the character locations, etc.
  • Type is the Typescript "type" to diagnose type safety.

Therefore, we should always get the Symbol first, and request for Node or Type if necessary.

For example, we could get Symbol from source code like this:

checker.getSymbolAtLocation(source);
Enter fullscreen mode Exit fullscreen mode

You may wonder what is the "checker" here, we will talk about it in the next section.

With the Symbol, we could find the corresponding Nodes according to the node type, like following:

symbol.declarations;
symbol.valueDeclaration;
Enter fullscreen mode Exit fullscreen mode

If we require to work with type then we may get it with Symbol and Node:

checker.getTypeOfSymbolAtLocation(mySymbol, myNode);
Enter fullscreen mode Exit fullscreen mode

Or get from the type Node directly:

checker.getTypeFromTypeNode(myNode);
Enter fullscreen mode Exit fullscreen mode

Initiating the type checking utility

To figure out the type of a declaration, we will need to initiate a Typescript type checker and parse our source code to the Typescript Symbol first.

import ts from "typescript";
import assert from "assert/strict";

const program = ts.createProgram({
  rootNames: [file], // A set of root files
  options: {}, // The compiler options
  // There are 3 other options available but I am not sure what they are used for yet
});
const source = program.getSourceFile(file);
assert(source);
const checker = program.getTypeChecker();
const moduleSymbol = checker.getSymbolAtLocation(source);
assert(moduleSymbol);
Enter fullscreen mode Exit fullscreen mode

Finding the exports of an ES module

With the type checker and symbol we get above, continue analysing from the ES module exports are a lot easier than with Babel:

const exports = checker.getExportsOfModule(moduleSymbol);
exports.forEach((ex) => {
  ex.declarations.forEach((d) => {
    // ...
  });
});
Enter fullscreen mode Exit fullscreen mode

Printing the closest type name

The type checker could provide the closest type name with helper:

const myType = checker.getTypeOfSymbolAtLocation(
  mySymbol,
  mySymbol.valueDeclaration!
);
checker.typeToString(myType);
Enter fullscreen mode Exit fullscreen mode

Well, this name is honestly not very helpful in identifying whether this identifier is the right type we are looking for as we often use union or extended types. Unfortunately, I haven't found a better way yet. One possibility I could think of is to create a piece of code and request Typescript to diagnose it with the whole project.

Identifying the type syntax with flags

Typescript compiler defined several flags including SymbolFlags, SyntaxKind, ModifierFlags, etc. I found the most helpful one is SyntaxKind. SyntaxKind could help to identify the type keywords from a Node in the following way:

if (myNode.kind === ts.SyntaxKind.NullKeyword) {
  // This is a null type, do something
}
Enter fullscreen mode Exit fullscreen mode

While some flags like ModifierFlags require a bit more operations:

if (ts.getCombinedModifierFlags(myNode) & (ts.ModifierFlags.Export !== 0);) {
  // I am exported, do something
}
Enter fullscreen mode Exit fullscreen mode

Identifying the Node type with ts helpers

The compiler provides a set of helpers to identify what kind of code we are dealing with. For example:

ts.isFunctionDeclaration(myNode);
ts.isArrowFunction(myNode);
ts.isVariableDeclaration(myNode);
ts.isIdentifier(myNode);
// ... and you can get more from auto-complete
Enter fullscreen mode Exit fullscreen mode

These helpers work with Node, and look quite straightforward from their name if you are familiar with the syntax definitions. However the list of helpers too long to find the right one sometimes as Typescript tried to work with ECMA, JSDoc and its typings.

Get Type object of a function

In some cases, we may want to know the parameters and return type of a function or construct. To achieve this, we will start from the Signature of them which could get from the Type object:

checker
  .getTypeOfSymbolAtLocation(mySymbol, mySymbol.valueDeclaration)
  .getConstructSignatures()
  .map((signature) => ({
    parameters: signature.parameters.map((paramSymbol) =>
      checker.getTypeOfSymbolAtLocation(
        paramSymbol,
        paramSymbol.valueDeclaration
      )
    ),
    returnType: signature.getReturnType(), // returns Type object
  }));
Enter fullscreen mode Exit fullscreen mode

Latest comments (0)