Processing TypeScript using TypeScript

2019-01-19

One of the interesting things about TypeScript is that it exposes a compiler API that you can use to process TypeScript code programmatically, from your own TypeScript code. This post will give you some idea of how this can be done and why it can be useful.

The TypeScript compiler API

When writing an application using TypeScript, you typically use the “typescript” module as a build tool to transpile your TypeScript code into JavaScript. This is usually all you need. However, if you import the “typescript” module in your application code, you get access to the compiler API. This compiler API provides some very powerful tools for interacting with TypeScript code. Some of its features are documented on the TypeScript wiki:Using the Compiler API.

SourceFiles and the abstract syntax tree (AST)

A SourceFile is perhaps the most basic form of processed TypeScript code. A TypeScript SourceFile contains a representation of the source code itself, from which you can extract theabstract syntax tree (AST) for the code. An AST represents the syntactical structure of the program as a tree, starting from the SourceFile itself and drilling down into the statements and their building blocks. In general, compilers or interpreters typically construct an AST as an initial step in the processing of the source code.

As an example, consider the following simple TypeScript code:

consttest:number=1+2;

Now, let’s write some code that creates a SourceFile for this code and prints the AST.

import*astsfrom"typescript";constfilename="test.ts";constcode=`const test: number = 1 + 2;`;constsourceFile=ts.createSourceFile(filename,code,ts.ScriptTarget.Latest);functionprintRecursiveFrom(node:ts.Node,indentLevel:number,sourceFile:ts.SourceFile){constindentation="-".repeat(indentLevel);constsyntaxKind=ts.SyntaxKind[node.kind];constnodeText=node.getText(sourceFile);console.log(`${indentation}${syntaxKind}:${nodeText}`);node.forEachChild(child=>printRecursiveFrom(child,indentLevel+1,sourceFile));}printRecursiveFrom(sourceFile,0,sourceFile);

This prints the following AST:

SourceFile: const test: number = 1 + 2;-VariableStatement: const test: number = 1 + 2;--VariableDeclarationList: const test: number = 1 + 2---VariableDeclaration: test: number = 1 + 2----Identifier: test----NumberKeyword: number----BinaryExpression: 1 + 2-----FirstLiteralToken: 1-----PlusToken: +-----FirstLiteralToken: 2-EndOfFileToken:

Here, we usedts.Node.forEachChild() to get the children for a node in the AST. There is an alternative to this,ts.Node.getChildren(sourceFile).forEach(), which creates a more detailed AST:

SourceFile: const test: number = 1 + 2;-SyntaxList: const test: number = 1 + 2;--VariableStatement: const test: number = 1 + 2;---VariableDeclarationList: const test: number = 1 + 2----ConstKeyword: const----SyntaxList: test: number = 1 + 2-----VariableDeclaration: test: number = 1 + 2------Identifier: test------ColonToken: :------NumberKeyword: number------FirstAssignment: =------BinaryExpression: 1 + 2-------FirstLiteralToken: 1-------PlusToken: +-------FirstLiteralToken: 2---SemicolonToken: ;-EndOfFileToken:

Looking at generated ASTs is an interesting way to learn more about the TypeScript language and how the compiler represents source code internally. If you want to look at ASTs in a more interactive way and view more information for the nodes in the tree, I recommend usingTypeScript AST Viewer.

Turning code into a Program

While SourceFiles are easy to create, the functionality they offer doesn’t always suffice. In order to do more interesting things like getting diagnostics or using the type checker, you need a Program.

Obtaining a Program from a file on disk is pretty straightforward. As the following example shows, this can be a one-liner in very simple cases.

constprogram=ts.createProgram(["src/test.ts"],{});

Getting a Program from a simple string of TypeScript code is a bit more tricky. In order to accomplish this, we need to specify a custom CompilerHost instance that will be used by the compiler to retrieve and write files.

import*astsfrom"typescript";constfilename="test.ts";constcode=`const test: number = 1 + 2;`;constsourceFile=ts.createSourceFile(filename,code,ts.ScriptTarget.Latest);constdefaultCompilerHost=ts.createCompilerHost({});constcustomCompilerHost:ts.CompilerHost={getSourceFile:(name,languageVersion)=>{console.log(`getSourceFile${name}`);if(name===filename){returnsourceFile;}else{returndefaultCompilerHost.getSourceFile(name,languageVersion);}},writeFile:(filename,data)=>{},getDefaultLibFileName:()=>"lib.d.ts",useCaseSensitiveFileNames:()=>false,getCanonicalFileName:filename=>filename,getCurrentDirectory:()=>"",getNewLine:()=>"\n",getDirectories:()=>[],fileExists:()=>true,readFile:()=>""};constprogram=ts.createProgram(["test.ts"],{},customCompilerHost);// getSourceFile test.ts// getSourceFile lib.d.ts

As you can see, thegetSourceFile method of the CompilerHost is called twice: once for getting the actual code we want to compile and once for gettinglib.d.ts, the default library specifying the JavaScript/TypeScript features that are available to the code.

Transpiling code

Transpiling code from TypeScript to plain JavaScript using the compiler API is pretty straightforward. In its simplest form, it takes one line of code.

constcode=`const test: number = 1 + 2;`;consttranspiledCode=ts.transpileModule(code,{}).outputText;console.log(transpiledCode);// var test = 1 + 2;

It is possible to pass several options to thetranspileModule method, including the compiler options to use.

You can also transpile code by invoking theemit() method on a Program. If you do this for a program created in the most simple way from an actual file on disk, this will put a transpiled .js file next to it. As an example, let’s write a piece of TypeScript code that will transpile itself when run.

// file test.tsimport*astsfrom"typescript";constprogram=ts.createProgram(["src/test.ts"],{});program.emit();

When this code is transpiled and run, it creates the following JavaScript file next to the test.ts file:

// file test.js"use strict";exports.__esModule=true;varts=require("typescript");varprogram=ts.createProgram(["src/test.ts"],{});program.emit();

It is also interesting to note that, if the file you transpile imports other TypeScript files, those will also be transpiled if the compiler can find them.

Getting diagnostics

If you have a Program, you can use that Program to obtain diagnostics for the code. In order to get the compiler errors or warnings, use thegetPreEmitDiagnostics() method. As an example, take a look at the following code which prints its own diagnostics.

import*astsfrom"typescript";lettest:number="test";// compiler errorconstprogram=ts.createProgram(["src/test.ts"],{});constdiagnostics=ts.getPreEmitDiagnostics(program);for(constdiagnosticofdiagnostics){constmessage=diagnostic.messageText;constfile=diagnostic.file;constfilename=file.fileName;constlineAndChar=file.getLineAndCharacterOfPosition(diagnostic.start);constline=lineAndChar.line+1;constcharacter=lineAndChar.character+1;console.log(message);console.log(`(${filename}:${line}:${character})`);}// Type '"test"' is not assignable to type 'number'.// (src/test.ts:3:5)

Getting type information

Another thing that a Program allows you to do is to obtain a TypeChecker for extracting type information from nodes in the AST. The following code obtains a TypeChecker for itself and uses the checker to emit the types of all variable declarations in the code.

import*astsfrom"typescript";constfilename="src/test.ts";constprogram=ts.createProgram([filename],{});constsourceFile=program.getSourceFile(filename);consttypeChecker=program.getTypeChecker();functionrecursivelyPrintVariableDeclarations(node:ts.Node,sourceFile:ts.SourceFile){if(ts.isVariableDeclaration(node)){constnodeText=node.getText(sourceFile);consttype=typeChecker.getTypeAtLocation(node);consttypeName=typeChecker.typeToString(type,node);console.log(nodeText);console.log(`(${typeName})`);}node.forEachChild(child=>recursivelyPrintVariableDeclarations(child,sourceFile));}recursivelyPrintVariableDeclarations(sourceFile,sourceFile);// filename = "src/test.ts"// ("src/test.ts")// program = ts.createProgram([filename], {})// (ts.Program)// sourceFile = program.getSourceFile(filename)// (ts.SourceFile)// typeChecker = program.getTypeChecker()// (ts.TypeChecker)// nodeText = node.getText(sourceFile)// (string)// type = typeChecker.getTypeAtLocation(node)// (ts.Type)// typeName = typeChecker.typeToString(type, node)// (string)

Use case: creating a custom linter

The TypeScript compiler API makes it pretty straightforward to create your own custom linter that generates errors or warnings if it finds certain things in the code. For an example, see this part of the compiler API documentation:Traversing the AST with a little linter. Note that the code uses the kind SyntaxKind of the node (node.kind) to determine the kind of node and then casts the node to its specific type, allowing for convenient access to certain child nodes.

The example above doesn’t create a Program, because there is no need to create one. If the information in the AST suffices for your linter, it is easier and more efficient to just create a SourceFile directly. More advanced linters may need type checking, which means you will need to generate a Program for the code to be linted in order to obtain a TypeChecker.

Use case: extracting type documentation

The documentation for the compiler API includes an example that uses a TypeChecker to extract and emit type documentation for the code:Using the Type Checker

Use case: altering or creating code programmatically

It is possible that you want to analyze some TypeScript source code and then alter it in some cases. One way to do this is to traverse the AST and generate a list of changes you want to perform on the code (e.g., remove 2 characters starting from position 11 and insert the string “test” instead). Then, take the source code as a string and apply the changes in reverse order (starting from the end of the source code, so your changes don’t affect the positions where the other changes need to happen).

You can also programmatically create AST nodes and use them to programmatically generate new TypeScript code.

import*astsfrom"typescript";conststatement=ts.createVariableStatement([],ts.createVariableDeclarationList([ts.createVariableDeclaration(ts.createIdentifier("testVar"),ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),ts.createStringLiteral("test"))],ts.NodeFlags.Const));constprinter=ts.createPrinter();constresult=printer.printNode(ts.EmitHint.Unspecified,statement,undefined);console.log(result);// const testVar: string = "test";

The TypeScript compiler API also allows you to define transformers that walk the AST and replace nodes by new ones as needed. The following code finds all identifiers in theSourceFile and adds a suffixsuffix to them.

import*astsfrom"typescript";constfilename="test.ts";constcode=`const test: number = 1 + 2;`;constsourceFile=ts.createSourceFile(filename,code,ts.ScriptTarget.Latest);consttransformerFactory:ts.TransformerFactory<ts.Node>=(context:ts.TransformationContext)=>{return(rootNode)=>{functionvisit(node:ts.Node):ts.Node{node=ts.visitEachChild(node,visit,context);if(ts.isIdentifier(node)){returnts.createIdentifier(node.text+"suffix");}else{returnnode;}}returnts.visitNode(rootNode,visit);};};consttransformationResult=ts.transform(sourceFile,[transformerFactory]);consttransformedSourceFile=transformationResult.transformed[0];constprinter=ts.createPrinter();constresult=printer.printNode(ts.EmitHint.Unspecified,transformedSourceFile,undefined);console.log(result);// const testsuffix: number = 1 + 2;

Resources