Using the TypeScript complier API
Disclaimer
Keep in mind that this is still an incomplete API - we set its release version to 0.5 and things will change over time.After all, there are some imperfections in the first iteration.I hope the community will give more feedback to improve the API.To allow users to convert between future versions, we will record significant API changes for each new version.
Set up
First you need to install TypeScript using npm with a version higher than 1.6.
After installation, you need to link TypeScript in the project path.Otherwise, the link will be directly global.
npm install -g typescript npm link typescript Copy Code
For some examples, you also need a node definition file.Run the following command to get the definition file:
npm install @types/node Copy Code
Once you've done that, you can try the following examples.
The compiler API has several main components:
- Program is a TypeScript term for your entire application
- CompilerHost is a user system API for reading files, checking directories, and case sensitivity.
- SourceFiles represents the source files in the application and contains both text and TypeScript AST.
A small complier
This example is a basic editor that gets a list of files for TypeScript and compiles them into the appropriate JavaScript.
We can create a Program through createProgram - this creates a default ComplierHost that uses the file system to get the files.
import * as ts from "typescript" function complier (fileNames: string[], options: ts.CompilerOptions): void { let program = ts.createProgram(fileNames, options) let emitResult = program.emit() let allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics) allDiagnostics.forEach(diagnostics => { if (diagnostics.file) { let { line, character } = diagnostics.file.getLineAndCharacterOfPosition(diagnostics.start!) let message = ts.flattenDiagnosticMessageText(diagnostics.messageText, '\n'); console.log(`${diagnostics.file.fileName} (${line + 1}, ${character + 1}): ${message}`); } else { console.log(ts.flattenDiagnosticMessageText(diagnostics.messageText, '\n')) } }) let exitCode = emitResult.emitSkipped ? 1 : 0; console.log(`Process exiting with code ${exitCode}.`); process.exit(exitCode) } complier(process.argv.slice(2), { noEmitOnError: true, noImplicitAny: true, target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS }) Copy Code
A simple transform function
It doesn't take much code to create a complier, but you might just want to get the appropriate JavaScript output given the TypeScript source file.Therefore, you can use ts.transplieModule to get a string => string code conversion from two lines of code.
import * as ts from "typescript"; const source = "let x: string = 'string'"; let result = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS } }) console.log(JSON.stringify(result)); Copy Code
Getting DTS from a JavaScript file
It will only run on TypeScript versions 3.7 and above.This example illustrates how to get a list of JavaScript files and display their generated d.ts files on the terminal.
import * as ts from 'typescript' function compile(fileNames: string[], options: ts.CompilerOptions): void { // Create program with emit in memory const createdFiles = {} const host = ts.createCompilerHost(options) host.writeFile = (fileName: string, contents: string) => createdFiles[fileName] = contents; // Prepare and emit the d.ts file const program = ts.createProgram(fileNames, options, host); program.emit() // Traverse all input files fileNames.forEach(file => { console.log('### JavaScript\n'); console.log(host.readFile(file)); console.log('### Type Defination\n'); const dts = file.replace('.js', '.d.ts') console.log(createdFiles[dts]); }) } // Run complier compile(process.argv.slice(2), { allowJs: true, declaration: true, emitDeclarationOnly: true, }) Copy Code
Reprint portions of TypeScript files
This example will undo the Typescript subpart of the JavaScript source file, which is useful when you want your application's code to be the real source.For example, the export is displayed with JSDoc comments.
import * as ts from 'typescript' /** * Print out a specific node from the source file * * @param file a path to a file * @param identifiers top level identifiers available */ function extract(file: string, identifiers: string[]): void { // Create a Program that represents a project // Then take out its source file to parse its AST let program = ts.createProgram([file], { allowJs: true }); const sourceFile = program.getSourceFile(file) // In order to print AST, we will use the printer of TypeScript const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); // To give constructive error messages, keep track of identifiers found and not found const unfoundNodes = [], foundNodes = []; // The root node of the AST that traverses the source file, possibly with a top-level identifier // You can use https://ts-ast-viewer.com/to expand this list to view the AST of the file // Then use the same pattern below ts.forEachChild(sourceFile, node => { let name = '' // This is an incomplete set of AST nodes if (ts.isFunctionDeclaration(node)) { name = node.name.text; // Hide method body when printing node.body = undefined; } else if (ts.isVariableStatement(node)) { name = node.declarationList.declarations[0].name.getText(sourceFile); } else if (ts.isInterfaceDeclaration(node)) { name = node.name.text; } const container = identifiers.includes(name) ? foundNodes : unfoundNodes; container.push([name, node]) }); // Either print the node found or provide a list of identifiers found if (!foundNodes.length) { console.log(`Could not found any of ${identifiers.join(',')} in ${file}, found: ${unfoundNodes.filter(f => f[0].map(f=>f[0]).join(''))}`); process.exitCode = 1; } else { foundNodes.map(f => { const [name , node] = f; console.log('###' + name + '\n'); console.log(printer.printNode(ts.EmitHint.Unspecified, node, sourceFile)) + '\n'; }); } } // Run the extract function with the parameters of the script Copy Code
Traverse AST with a small checker
The Node interface is the root interface of TypeScript AST.Usually we use the forEachChild function to recursively traverse trees.This includes visitor patterns and often provides more flexibility.
As an example of how to traverse a file AST, consider the smallest inspector that does the following:
- Check that all loop statements are enclosed in curly brackets.
- Check that all if/else statements are enclosed in curly brackets.
- Is the strong symbol (===/!==) used to replace the loose one (== /!=).
import { readFileSync } from 'fs' import * as ts from 'typescript' export function delint (sourceFile: ts.SourceFile) { delintNode(sourceFile) function delintNode (node: ts.Node) { switch (node.kind) { case ts.SyntaxKind.ForStatement: case ts.SyntaxKind.ForInStatement: case ts.SyntaxKind.WhileStatement: case ts.SyntaxKind.DoStatement: if ((node as ts.IterationStatement).statement.kind !== ts.SyntaxKind.Block) { report( node, 'A looping statement\'s contents should be wrapped in a block body.' ) } break; case ts.SyntaxKind.IfStatement: const ifStatement = node as ts.IfStatement; if (ifStatement.thenStatement.kind !== ts.SyntaxKind.Block) { report(ifStatement.thenStatement, 'An if statement\'s contents should be wrapped in a block body.'); } if ( ifStatement.elseStatement && ifStatement.elseStatement.kind !== ts.SyntaxKind.Block && ifStatement.elseStatement.kind !== ts.SyntaxKind.IfStatement ) { report( ifStatement.elseStatement, 'An else statement\'s contents should be wrapped in a block body.' ); } break; case ts.SyntaxKind.BinaryExpression: const op = (node as ts.BinaryExpression).operatorToken.kind; if (op === ts.SyntaxKind.EqualsEqualsToken || op === ts.SyntaxKind.ExclamationEqualsToken) { report(node, 'Use \'==\' and \'!==\'.'); } break; } ts.forEachChild(node, delintNode) } function report (node: ts.Node, message: string) { const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); console.log(`${sourceFile.fileName} (${line + 1}, ${character + 1}): ${message}`); } } const fileNames = process.argv.slice(2); fileNames.forEach(fileName => { // Parse a file const sourceFile = ts.createSourceFile( fileName, readFileSync(fileName).toString(), ts.ScriptTarget.ES2015, /* setParentNodes */ true ); // delint it delint(sourceFile) }); Copy Code
In this example, we don't need to create a type checker because we just want to traverse each source file.
All ts.SyntaxKind can Enumeration Type Found in.
Write Incremental Program Monitor
TypeScript 2.7 introduced two new APIs: one for creating "watcher" programs and providing a set of APIs that trigger refactoring; the other "builder" API that watcher can use.BuilderPrograms is a program instance that caches errors and issues them on previously compiled modules if the module or its dependencies are not updated in a cascade manner."Watcher" programs can use generator program instances to update only the results of affected files (such as errors and issues) during compilation.This can speed up large projects with many files.
This API is used internally in compiler to implement the--watch pattern, but it can also be used by other tools, such as:
import ts = require("typescript") const formatHost: ts.FormatDiagnosticsHost = { getCanonicalFileName: path => path, getCurrentDirectory: ts.sys.getCurrentDirectory, getNewLine: () => ts.sys.newLine }; function watchMain () { const configPath = ts.findConfigFile( /*searchPath*/ "./", ts.sys.fileExists, "tsconfig.json" ); if (!configPath) { throw new Error("Could not find a valid 'tsconfig.json'."); } // TypeScript can create "policies" using several different programs: // * ts.createEmitAndSemanticDiagnosticsBuilderProgram // * ts.createSemanticDiagnosticsBuilderProgram // * ts.createAbstractBuilder // The first two API s produce the Generator program. // They use an incremental policy to only recheck and issue files whose contents may have changed // Or its dependencies may change, which may affect previous type checks and changes that issue results // The last API performs a full type check after each change. // The only difference between `createEmitAndSemanticDiagnosticsBuilderProgram` and `createSemanticDiagnosticsBuilderProgram` is that it does not emit // It is preferable to use `createSemanticDiagnosticsBuilderProgram` for pure type check scenarios or when another tool/process process process process process is issued const createProgram = ts.createSemanticDiagnosticsBuilderProgram; // Note that `createWatchCompilerHost` also has an overload that requires a set of root files. const host = ts.createWatchCompilerHost( configPath, {}, ts.sys, createProgram, reportDiagnostic, reportWatchStatusChanged ); // Technically, you can override any given hook function on the host, although you may not need to. // Note that we assume that `origCreateProgram'and `origPostProgramCreate' do not use `this'at all. const origCreateProgram = host.createProgram host.createProgram = (rootNames: ReadonlyArray<string>, options, host, oldProgram) => { console.log("** We're about to create the program! **"); return origCreateProgram(rootNames, options, host, oldProgram); }; const origPostProgramCreate = host.afterProgramCreate; host.afterProgramCreate = program => { console.log("** We finished making the program! **"); origPostProgramCreate!(program); }; // `createWatchProgram` Creates an initial program, monitors the file, and updates the program over time. ts.createWatchProgram(host); } function reportDiagnostic(diagnostic: ts.Diagnostic) { console.error("Error", diagnostic.code, ":", ts.flattenDiagnosticMessageText(diagnostic.messageText, formatHost.getNewLine())); } /** * Diagnostic information is printed every time a status change is monitored * This is mainly used for messages such as Start Compiling or Finish Compiling. */ function reportWatchStatusChanged (diagnostic: ts.Diagnostic) { console.info(ts.formatDiagnostic(diagnostic, formatHost)); } watchMain(); Copy Code
Incremental build support using language services
Can be referred to Using the Language Service API Get more details.
The service layer provides an additional layer of utilities that can help simplify some complex scenarios.In the code snippet below, we will try to build an incremental build server that monitors a set of files and only updates the output of files that have changed.We'll create an object called LanguageService to do this.Similar to the program in the previous example.We need a Language ServiceHost.LanguageServiceHost does this through version, isOpen flag, and criptSnapshot.This version allows language services to track file changes.IsOpen tells the language service to save the AST in memory when using the file.ScriptSnapshot is an abstraction of text that allows language services to query for changes.
If you just want to implement the monitoring style, you can study the monitor program API above.
import * as fs from 'fs' import * as ts from 'typescript' function watch (rootFileNames: string[], options: ts.CompilerOptions) { const files: ts.MapLike<{ version: number }> = {}; // Initialization File List rootFileNames.forEach(fileName => { files[fileName] = { version: 0 }; }); // Create a language service host to allow LS to communicate with the host const serviceHost: ts.LanguageServiceHost = { getScriptFileNames: () => rootFileNames, getScriptVersion: fileName => files[fileName] && files[fileName].version.toString(), getScriptSnapshot: fileName => { if (!fs.existsSync(fileName)) { return undefined } return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString()) }, getCurrentDirectory: () => process.cwd(), getCompilationSettings: () => options, getDefaultLibFileName: options => ts.getDefaultLibFilePath(options), fileExists: ts.sys.fileExists, readFile: ts.sys.readFile, readDirectory: ts.sys.readDirectory }; // Create Language Service File const services = ts.createLanguageService(serviceHost, ts.createDocumentRegistry()); // Start listening rootFileNames.forEach(fileName => { // First, issue all files emitFile(fileName); // Add a script above to listen for the next change fs.watchFile(fileName, { persistent: true, interval: 250 }, (curr, prev) => { // Check timestamp if (+curr.mtime <= +prev.mtime) { return; } // Updating version indicates that the file has been modified files[fileName].version ++ // Write changes to disk emitFile(fileName) }) }) function emitFile(fileName: string) { let output = services.getEmitOutput(fileName); if (!output.emitSkipped) { console.log(`Emitting ${fileName}`); } else { console.log(`Emitting ${fileName} failed`); logErrors(fileName) } output.outputFiles.forEach(o => { fs.writeFileSync(o.name, o.text, "utf8") }) } function logErrors(fileName: string) { let allDiagnostics = services .getCompilerOptionsDiagnostics() .concat(services.getSyntacticDiagnostics(fileName)) .concat(services.getSemanticDiagnostics(fileName)); allDiagnostics.forEach(diagnostic => { let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); if (diagnostic.file) { let { line, character } = diagnostic.file.getLineAndCharacterOfPosition( diagnostic.start! ); console.log(` Error ${diagnostic.file.fileName} (${line + 1}): ${message}`); } else { console.log(` Error: ${message} `); } }) } } // Initialize the files that make up the program to be all.ts files in the current directory const currentDirectoryFiles = fs.readdirSync(process.cwd()).filter(fileName => fileName.length >= 3 && fileName.substr(fileName.length - 3, 3) === '.ts'); // Start listening watch(currentDirectoryFiles, { module: ts.ModuleKind.CommonJS }) Copy Code
Custom Module Resolution
By implementing the optional method, you can override the standard way a compiler parses a module: CompilerHost.resolvedModuleNames:
CompilerHost.resolveModuleNames(moduleNames: string[], containingFile: string): string[]
This method gives a list of module names in a file and expects to return an array of moduleNames.length in size, where each element stores either of the following:
- Resolution of the corresponding name in the module Names array of the RelveModule instance with the non-empty property resolveFileName
- Return undefined if module name cannot be resolved
You can call the standard module resolution process by calling resolveModuleName:
resolveModuleName(moduleName: string, containingFile: string, options: CompilerOptions, moduleResolutionHost: ModuleResolutionHost): ResolvedModuleNameWithFallbackLocations
.
This function returns an object that stores the result of module resolution (the value of the resolvedModule property) and a list of file names considered candidates before making the current decision.
import * as ts from "typescript"; import * as path from "path"; function createCompilerHost(options: ts.CompilerOptions, moduleSearchLocations: string[]): ts.CompilerHost { return { getSourceFile, getDefaultLibFileName: () => "lib.d.ts", writeFile: (fileName, content) => ts.sys.writeFile(fileName, content), getCurrentDirectory: () => ts.sys.getCurrentDirectory(), getDirectories: path => ts.sys.getDirectories(path), getCanonicalFileName: fileName => ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(), getNewLine: () => ts.sys.newLine, useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames, fileExists, readFile, resolveModuleNames }; function fileExists(fileName: string): boolean { return ts.sys.fileExists(fileName); } function readFile(fileName: string): string | undefined { return ts.sys.readFile(fileName); } function getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) { const sourceText = ts.sys.readFile(fileName); return sourceText !== undefined ? ts.createSourceFile(fileName, sourceText, languageVersion) : undefined; } function resolveModuleNames( moduleNames: string[], containingFile: string ): ts.ResolvedModule[] { const resolvedModules: ts.ResolvedModule[] = []; for (const moduleName of moduleNames) { // try to use standard resolution let result = ts.resolveModuleName(moduleName, containingFile, options, { fileExists, readFile }); if (result.resolvedModule) { resolvedModules.push(result.resolvedModule); } else { // check fallback locations, for simplicity assume that module at location // should be represented by '.d.ts' file for (const location of moduleSearchLocations) { const modulePath = path.join(location, moduleName + ".d.ts"); if (fileExists(modulePath)) { resolvedModules.push({ resolvedFileName: modulePath }); } } } } return resolvedModules; } } function compile(sourceFiles: string[], moduleSearchLocations: string[]): void { const options: ts.CompilerOptions = { module: ts.ModuleKind.AMD, target: ts.ScriptTarget.ES5 }; const host = createCompilerHost(options, moduleSearchLocations); const program = ts.createProgram(sourceFiles, options, host); /// do something with program... } Copy Code
Create and print TypeScript AST
TypeScript has factory functions and a print API that you can use in conjunction with.
- Factory functions allow you to generate new tree nodes in the AST format of TypeScript.
- The print API takes an existing tree structure (generated by a createSourceFile or factory function) and generates an output string.
Here is an example of using these two methods to generate a factorial function:
import ts = require("typescript"); function makeFactorialFunction() { const functionName = ts.createIdentifier("factorial"); const paramName = ts.createIdentifier("n"); const parameter = ts.createParameter( /*decorators*/ undefined, /*modifiers*/ undefined, /*dotDotDotToken*/ undefined, paramName ); const condition = ts.createBinary(paramName, ts.SyntaxKind.LessThanEqualsToken, ts.createLiteral(1)); const ifBody = ts.createBlock([ts.createReturn(ts.createLiteral(1))], /*multiline*/ true); const decrementedArg = ts.createBinary(paramName, ts.SyntaxKind.MinusToken, ts.createLiteral(1)); const recurse = ts.createBinary(paramName, ts.SyntaxKind.AsteriskToken, ts.createCall(functionName, /*typeArgs*/ undefined, [decrementedArg])); const statements = [ts.createIf(condition, ifBody), ts.createReturn(recurse)]; return ts.createFunctionDeclaration( /*decorators*/ undefined, /*modifiers*/ [ts.createToken(ts.SyntaxKind.ExportKeyword)], /*asteriskToken*/ undefined, functionName, /*typeParameters*/ undefined, [parameter], /*returnType*/ ts.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), ts.createBlock(statements, /*multiline*/ true) ); } const resultFile = ts.createSourceFile("someFileName.ts", "", ts.ScriptTarget.Latest, /*setParentNodes*/ false, ts.ScriptKind.TS); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const result = printer.printNode(ts.EmitHint.Unspecified, makeFactorialFunction(), resultFile); console.log(result); Copy Code
Use Type Checker
In this example, we will iterate through AST and use a checker to serialize class information.We'll use a type checker to get symbols and type information, as well as JSDoc comments for the exported classes, their constructors, and their respective constructor parameters.
import * as ts from "typescript"; import * as fs from "fs"; interface DocEntry { name?: string; fileName?: string; documentation?: string; type?: string; constructors?: DocEntry[]; parameters?: DocEntry[]; returnType?: string; } /** Generate documentation for all classes in a set of .ts files */ function generateDocumentation( fileNames: string[], options: ts.CompilerOptions ): void { // Build a program using the set of root file names in fileNames let program = ts.createProgram(fileNames, options); // Get the checker, we will use it to find more about classes let checker = program.getTypeChecker(); let output: DocEntry[] = []; // Visit every sourceFile in the program for (const sourceFile of program.getSourceFiles()) { if (!sourceFile.isDeclarationFile) { // Walk the tree to search for classes ts.forEachChild(sourceFile, visit); } } // print out the doc fs.writeFileSync("classes.json", JSON.stringify(output, undefined, 4)); return; /** visit nodes finding exported classes */ function visit(node: ts.Node) { // Only consider exported nodes if (!isNodeExported(node)) { return; } if (ts.isClassDeclaration(node) && node.name) { // This is a top level class, get its symbol let symbol = checker.getSymbolAtLocation(node.name); if (symbol) { output.push(serializeClass(symbol)); } // No need to walk any further, class expressions/inner declarations // cannot be exported } else if (ts.isModuleDeclaration(node)) { // This is a namespace, visit its children ts.forEachChild(node, visit); } } /** Serialize a symbol into a json object */ function serializeSymbol(symbol: ts.Symbol): DocEntry { return { name: symbol.getName(), documentation: ts.displayPartsToString(symbol.getDocumentationComment(checker)), type: checker.typeToString( checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!) ) }; } /** Serialize a class symbol information */ function serializeClass(symbol: ts.Symbol) { let details = serializeSymbol(symbol); // Get the construct signatures let constructorType = checker.getTypeOfSymbolAtLocation( symbol, symbol.valueDeclaration! ); details.constructors = constructorType .getConstructSignatures() .map(serializeSignature); return details; } /** Serialize a signature (call or construct) */ function serializeSignature(signature: ts.Signature) { return { parameters: signature.parameters.map(serializeSymbol), returnType: checker.typeToString(signature.getReturnType()), documentation: ts.displayPartsToString(signature.getDocumentationComment(checker)) }; } /** True if this is visible outside this file, false otherwise */ function isNodeExported(node: ts.Node): boolean { return ( (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0 || (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) ); } } generateDocumentation(process.argv.slice(2), { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS }); Copy Code
Use this script:
tsc docGenerator.ts --m commonjs node docGenerator.js test.ts Copy Code
Enter the following example:
/** * Documentation for C */ class C { /** * constructor documentation * @param a my parameter documentation * @param b another parameter documentation */ constructor(a: string, b: C) { } } Copy Code
We can get this output:
[ { "name": "C", "documentation": "Documentation for C ", "type": "typeof C", "constructors": [ { "parameters": [ { "name": "a", "documentation": "my parameter documentation", "type": "string" }, { "name": "b", "documentation": "another parameter documentation", "type": "C" } ], "returnType": "C", "documentation": "constructor documentation" } ] } ] Copy Code