Identifying code with Typescript compiler
Tags: typescript, javascript
Published: 2022-06-18 23:03:07

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:

```typescript
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);
```

> Here my little VSCode plugin [Babel AST Explorer](https://marketplace.visualstudio.com/items?itemName=jyee721.babel-ast-explorer) helped me saved a lot of time.

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

```javascript
export default function f() {}
```

```javascript
export default () => {};
```

```javascript
export function f() {}
```

```javascript
export const f = () => {};
```

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

```javascript
function f() {}
export { f };
export const f2 = f;
export default f;
```

This brought me to the Typescript compiler.

However, the [document of Typescript compiler](https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API) 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-basic-definitions)
+ [Initiating the type checking utility](#initiating-the-type-checking-utility)
+ [Finding the exports of an ES module](#finding-the-exports-of-an-es-module)
+ [Printing the closest type name](#printing-the-closest-type-name)
+ [Identifying the type syntax with flags](#identifying-the-type-syntax-with-flags)
+ [Identifying the Node type with ts helpers](#identifying-the-node-type-with-ts-helpers)
+ [Get Type object of a function](#get-type-object-of-a-function)

## 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:

```typescript
checker.getSymbolAtLocation(source);
```

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:

```typescript
symbol.declarations;
symbol.valueDeclaration;
```

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

```typescript
checker.getTypeOfSymbolAtLocation(mySymbol, myNode);
```

Or get from the type Node directly:

```typescript
checker.getTypeFromTypeNode(myNode);
```

## 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.

```typescript
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);
```

## 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:

```typescript
const exports = checker.getExportsOfModule(moduleSymbol);
exports.forEach((ex) => {
  ex.declarations.forEach((d) => {
    // ...
  });
});
```

## Printing the closest type name

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

```typescript
const myType = checker.getTypeOfSymbolAtLocation(
  mySymbol,
  mySymbol.valueDeclaration!
);
checker.typeToString(myType);
```

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:

```typescript
if (myNode.kind === ts.SyntaxKind.NullKeyword) {
  // This is a null type, do something
}
```

While some flags like `ModifierFlags` require a bit more operations:

```typescript
if (ts.getCombinedModifierFlags(myNode) & (ts.ModifierFlags.Export !== 0);) {
  // I am exported, do something
}
```

## 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:

```typescript
ts.isFunctionDeclaration(myNode);
ts.isArrowFunction(myNode);
ts.isVariableDeclaration(myNode);
ts.isIdentifier(myNode);
// ... and you can get more from auto-complete
```

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:

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