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);
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() {}
export default () => {};
export function f() {}
export const f = () => {};
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;
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
- Initiating the type checking utility
- Finding the exports of an ES module
- Printing the closest type name
- Identifying the type syntax with flags
- Identifying the Node type with ts helpers
- 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:
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:
symbol.declarations;
symbol.valueDeclaration;
If we require to work with type then we may get it with Symbol and Node:
checker.getTypeOfSymbolAtLocation(mySymbol, myNode);
Or get from the type Node directly:
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.
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:
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:
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:
if (myNode.kind === ts.SyntaxKind.NullKeyword) {
// This is a null type, do something
}
While some flags like ModifierFlags
require a bit more operations:
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:
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:
checker
.getTypeOfSymbolAtLocation(mySymbol, mySymbol.valueDeclaration)
.getConstructSignatures()
.map((signature) => ({
parameters: signature.parameters.map((paramSymbol) =>
checker.getTypeOfSymbolAtLocation(
paramSymbol,
paramSymbol.valueDeclaration
)
),
returnType: signature.getReturnType(), // returns Type object
}));
Top comments (0)