Using the Compiler API

Daniel Rosenwasser edited this page Aug 18, 2016 · 52 revisions

##Disclaimer Keep in mind that this is not yet a stable API - we’re releasing this as version 0.5, and things will be changing over time. As a first iteration, there will be a few rough edges. We encourage any and all feedback from the community to improve the API. To allow users to transition between future releases, we will be documenting any API Breaking Changes per new release.

Getting set up

First you'll need to install TypeScript >=1.6 from npm.

For API Samples compatible with TypeScript == 1.4 please see Using the Compiler API (TypeScript 1.4)

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that will take a list of TypeScript files and compile down to their corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";functioncompile(fileNames: string[],options: ts.CompilerOptions): void{letprogram=ts.createProgram(fileNames,options);letemitResult=program.emit();letallDiagnostics=ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);allDiagnostics.forEach(diagnostic=>{let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,'\n');console.log(`${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);});letexitCode=emitResult.emitSkipped ? 1 : 0;console.log(`Process exiting with code '${exitCode}'.`);process.exit(exitCode);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may want to just get the corresponding JavaScript output given TypeScript sources. For this you can use ts.transpileModule to get a string => string transformation in two lines.

import*astsfrom"typescript";constsource="let x: string = 'string'";letresult=ts.transpileModule(source,{compilerOptions: {module: ts.ModuleKind.CommonJS}});console.log(JSON.stringify(result));

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />import{readFileSync}from"fs";import*astsfrom"typescript";exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
letifStatement=(<ts.IfStatement>node);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;casets.SyntaxKind.BinaryExpression:
letop=(<ts.BinaryExpression>node).operatorToken.kind;if(op===ts.SyntaxKind.EqualsEqualsToken||op==ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){let{ line, character }=sourceFile.getLineAndCharacterOfPosition(node.getStart());console.log(`${sourceFile.fileName} (${line+1},${character+1}): ${message}`);}}constfileNames=process.argv.slice(2);fileNames.forEach(fileName=>{// Parse a fileletsourceFile=ts.createSourceFile(fileName,readFileSync(fileName).toString(),ts.ScriptTarget.ES6,/*setParentNodes */true);// delint itdelint(sourceFile);});

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and updates only the outputs of the files that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version allows the language service to track changes to files. isOpen tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />import*asfsfrom"fs";import*astsfrom"typescript";functionwatch(rootFileNames: string[],options: ts.CompilerOptions){constfiles: ts.Map<{version: number}>={};// initialize the list of filesrootFileNames.forEach(fileName=>{files[fileName]={version: 0};});// Create the language service host to allow the LS to communicate with the hostconstservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>rootFileNames,getScriptVersion: (fileName)=>files[fileName]&&files[fileName].version.toString(),getScriptSnapshot: (fileName)=>{if(!fs.existsSync(fileName)){returnundefined;}returnts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString());},getCurrentDirectory: ()=>process.cwd(),getCompilationSettings: ()=>options,getDefaultLibFileName: (options)=>ts.getDefaultLibFilePath(options),};// Create the language service filesconstservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesrootFileNames.forEach(fileName=>{// First time around, emit all filesemitFile(fileName);// Add a watch on the file to handle next changefs.watchFile(fileName,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}// Update the version to signal a change in the filefiles[fileName].version++;// write the changes to diskemitFile(fileName);});});functionemitFile(fileName: string){letoutput=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");});}functionlogErrors(fileName: string){letallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(fileName)).concat(services.getSemanticDiagnostics(fileName));allDiagnostics.forEach(diagnostic=>{letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,"\n");if(diagnostic.file){let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);console.log(` Error ${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);}else{console.log(` Error: ${message}`);}});}}// Initialize files constituting the program as all .ts files in the current directoryconstcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(fileName=>fileName.length>=3&&fileName.substr(fileName.length-3,3)===".ts");// Start the watcherwatch(currentDirectoryFiles,{module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){letoptions=getDefaultOptions();// Parse the source textletsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,/*setParentPointers*/true);// Get the formatting edits on the input sourcesletedits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.letruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textletresult=text;for(leti=edits.length-1;i>=0;i--){letchange=edits[i];lethead=result.slice(0,change.span.start);lettail=result.slice(change.span.start+change.span.length)result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}}letcode="var a=function(v:number){return 0+1+2+3;\n}";letresult=format(code);console.log(result);

Transpiling a single file

Currently TypeScript exposes two functions for this purpose: transpileModule and transpile (which is deprecated). Note that regardless of the name, each one assumes that the input file is a module.

Here is the relevant signature of transpileModule:

exportinterfaceTranspileOptions{compilerOptions?: CompilerOptions;fileName?: string;reportDiagnostics?: boolean;moduleName?: string;renamedDependencies?: Map<string>;}exportinterfaceTranspileOutput{outputText: string;diagnostics?: Diagnostic[];sourceMapText?: string;}/* * This function will compile source text from 'input' argument using specified compiler options. * If not options are provided - it will use a set of default compiler options. * Extra compiler options that will unconditionally be used by this function are: * - isolatedModules = true * - allowNonTsExtensions = true * - noLib = true * - noResolve = true */exportfunctiontranspileModule(input: string,transpileOptions: TranspileOptions): TranspileOutput

and here is the appropriate version of transpile:

exportfunctiontranspile(input: string,compilerOptions?: CompilerOptions,fileName?: string,diagnostics?: Diagnostic[],moduleName?: string): string;

Historical note: initially only transpile function existed, however it was pretty difficult to extend (i.e to add new input parameters or return some extra information like source maps) without breaking existing consumers. As a result transpile is currently considered deprecated and superseded by transpileModule.

varts=require("typescript");varcontent="import {f} from \"foo\"\n"+"export var x = f()";varcompilerOptions={module: ts.ModuleKind.System};varres1=ts.transpileModule(content,{compilerOptions: compilerOptions,moduleName: "myModule2"});console.log(res1.outputText);console.log("============")varres2=ts.transpile(content,compilerOptions,/*fileName*/undefined,/*diagnostics*/undefined,/*moduleName*/"myModule1");console.log(res2);

Usually TypeScript compiler uses file extension to determine if file should be parsed as '.tsx' or '.ts'. The same rule is applied during single file transpilation if the file name is provided. If the file name is omitted, then compiler will check if the jsx options is specified. If it is set and is not JsxEmit.None, then source text will be interpreted as '.tsx'.

Customizing module resolution

You can override the standard way the compiler uses to resolve modules by implementing optional method: CompilerHost.resolveModuleNames:

CompilerHost.resolveModuleNames(moduleNames: string[], containingFile: string): string[].

The method is given a list of module names in a file, and is expected to return an array of size moduleNames.length, each element of the array stores either:

  • an instance of ResolvedModule with non-empty property resolvedFileName - resolution for corresponding name from moduleNames array or
  • undefined if module name cannot be resolved.

You can invoke the standard module resolution process via calling resolveModuleName:

resolveModuleName(moduleName: string, containingFile: string, options: CompilerOptions, moduleResolutionHost: ModuleResolutionHost): ResolvedModuleNameWithFallbackLocations.

This function returns an object that stores result of module resolution (value of resolvedModule property) as well as list of file names that were considered candidates before making current decision.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*aspathfrom"path";functioncreateCompilerHost(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(),getCanonicalFileName: fileName=>ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),getNewLine: ()=>ts.sys.newLine,useCaseSensitiveFileNames: ()=>ts.sys.useCaseSensitiveFileNames,
fileExists,
readFile,
resolveModuleNames
}functionfileExists(fileName: string): boolean{returnts.sys.fileExists(fileName);}functionreadFile(fileName: string): string{returnts.sys.readFile(fileName);}functiongetSourceFile(fileName: string,languageVersion: ts.ScriptTarget,onError?: (message: string)=>void){constsourceText=ts.sys.readFile(fileName);returnsourceText!==undefined ? ts.createSourceFile(fileName,sourceText,languageVersion) : undefined;}functionresolveModuleNames(moduleNames: string[],containingFile: string): ts.ResolvedModule[]{returnmoduleNames.map(moduleName=>{// try to use standard resolutionletresult=ts.resolveModuleName(moduleName,containingFile,options,{fileExists, readFile});if(result.resolvedModule){returnresult.resolvedModule;}// check fallback locations, for simplicity assume that module at location should be represented by '.d.ts' filefor(constlocationofmoduleSearchLocations){constmodulePath=path.join(location,moduleName+".d.ts");if(fileExists(modulePath)){return{resolvedFileName: modulePath}}}returnundefined;});}}functioncompile(sourceFiles: string[],moduleSearchLocations: string[]): void{constoptions: ts.CompilerOptions={module: ts.ModuleKind.AMD,target: ts.ScriptTarget.ES5};consthost=createCompilerHost(options,moduleSearchLocations);constprogram=ts.createProgram(sourceFiles,options,host);/// do something with program...}

Using the Type Checker

In this example we will walk the AST and use the checker to serialize class information. We'll use the type checker to get symbol and type information, while grabbing JSDoc comments for exported classes, their constructors, and respective constructor parameters.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*asfsfrom"fs";interfaceDocEntry{name?: string,fileName?: string,documentation?: string,type?: string,constructors?: DocEntry[],parameters?: DocEntry[],returnType?: string};/** Generate documention for all classes in a set of .ts files */functiongenerateDocumentation(fileNames: string[],options: ts.CompilerOptions): void{// Build a program using the set of root file names in fileNamesletprogram=ts.createProgram(fileNames,options);// Get the checker, we will use it to find more about classesletchecker=program.getTypeChecker();letoutput: DocEntry[]=[];// Visit every sourceFile in the program for(constsourceFileofprogram.getSourceFiles()){// Walk the tree to search for classests.forEachChild(sourceFile,visit);}// print out the docfs.writeFileSync("classes.json",JSON.stringify(output,undefined,4));return;/** visit nodes finding exported classes */functionvisit(node: ts.Node){// Only consider exported nodesif(!isNodeExported(node)){return;}if(node.kind===ts.SyntaxKind.ClassDeclaration){// This is a top level class, get its symbolletsymbol=checker.getSymbolAtLocation((<ts.ClassDeclaration>node).name);output.push(serializeClass(symbol));// No need to walk any further, class expressions/inner declarations// cannot be exported}elseif(node.kind===ts.SyntaxKind.ModuleDeclaration){// This is a namespace, visit its childrents.forEachChild(node,visit);}}/** Serialize a symbol into a json object */functionserializeSymbol(symbol: ts.Symbol): DocEntry{return{name: symbol.getName(),documentation: ts.displayPartsToString(symbol.getDocumentationComment()),type: checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration))};}/** Serialize a class symbol infomration */functionserializeClass(symbol: ts.Symbol){letdetails=serializeSymbol(symbol);// Get the construct signaturesletconstructorType=checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration);details.constructors=constructorType.getConstructSignatures().map(serializeSignature);returndetails;}/** Serialize a signature (call or construct) */functionserializeSignature(signature: ts.Signature){return{parameters: signature.parameters.map(serializeSymbol),returnType: checker.typeToString(signature.getReturnType()),documentation: ts.displayPartsToString(signature.getDocumentationComment())};}/** True if this is visible outside this file, false otherwise */functionisNodeExported(node: ts.Node): boolean{return(node.flags&ts.NodeFlags.Export)!==0||(node.parent&&node.parent.kind===ts.SyntaxKind.SourceFile);}}generateDocumentation(process.argv.slice(2),{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

to try this:

tsc docGenerator.ts --m commonjs node docGenerator.js test.ts

Passing an input like:

/**  * Documentation for C  */classC{/**  * constructor documentation * @param a my parameter documentation * @param b another parameter documentation */constructor(a: string,b: C){}}

We should get output like:

[
{
"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"
}
]
}
]

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Using the Compiler API

Daniel Rosenwasser edited this page Aug 18, 2016 · 52 revisions

##Disclaimer Keep in mind that this is not yet a stable API - we’re releasing this as version 0.5, and things will be changing over time. As a first iteration, there will be a few rough edges. We encourage any and all feedback from the community to improve the API. To allow users to transition between future releases, we will be documenting any API Breaking Changes per new release.

Getting set up

First you'll need to install TypeScript >=1.6 from npm.

For API Samples compatible with TypeScript == 1.4 please see Using the Compiler API (TypeScript 1.4)

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that will take a list of TypeScript files and compile down to their corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";functioncompile(fileNames: string[],options: ts.CompilerOptions): void{letprogram=ts.createProgram(fileNames,options);letemitResult=program.emit();letallDiagnostics=ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);allDiagnostics.forEach(diagnostic=>{let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,'\n');console.log(`${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);});letexitCode=emitResult.emitSkipped ? 1 : 0;console.log(`Process exiting with code '${exitCode}'.`);process.exit(exitCode);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may want to just get the corresponding JavaScript output given TypeScript sources. For this you can use ts.transpileModule to get a string => string transformation in two lines.

import*astsfrom"typescript";constsource="let x: string = 'string'";letresult=ts.transpileModule(source,{compilerOptions: {module: ts.ModuleKind.CommonJS}});console.log(JSON.stringify(result));

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />import{readFileSync}from"fs";import*astsfrom"typescript";exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
letifStatement=(<ts.IfStatement>node);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;casets.SyntaxKind.BinaryExpression:
letop=(<ts.BinaryExpression>node).operatorToken.kind;if(op===ts.SyntaxKind.EqualsEqualsToken||op==ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){let{ line, character }=sourceFile.getLineAndCharacterOfPosition(node.getStart());console.log(`${sourceFile.fileName} (${line+1},${character+1}): ${message}`);}}constfileNames=process.argv.slice(2);fileNames.forEach(fileName=>{// Parse a fileletsourceFile=ts.createSourceFile(fileName,readFileSync(fileName).toString(),ts.ScriptTarget.ES6,/*setParentNodes */true);// delint itdelint(sourceFile);});

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and updates only the outputs of the files that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version allows the language service to track changes to files. isOpen tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />import*asfsfrom"fs";import*astsfrom"typescript";functionwatch(rootFileNames: string[],options: ts.CompilerOptions){constfiles: ts.Map<{version: number}>={};// initialize the list of filesrootFileNames.forEach(fileName=>{files[fileName]={version: 0};});// Create the language service host to allow the LS to communicate with the hostconstservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>rootFileNames,getScriptVersion: (fileName)=>files[fileName]&&files[fileName].version.toString(),getScriptSnapshot: (fileName)=>{if(!fs.existsSync(fileName)){returnundefined;}returnts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString());},getCurrentDirectory: ()=>process.cwd(),getCompilationSettings: ()=>options,getDefaultLibFileName: (options)=>ts.getDefaultLibFilePath(options),};// Create the language service filesconstservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesrootFileNames.forEach(fileName=>{// First time around, emit all filesemitFile(fileName);// Add a watch on the file to handle next changefs.watchFile(fileName,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}// Update the version to signal a change in the filefiles[fileName].version++;// write the changes to diskemitFile(fileName);});});functionemitFile(fileName: string){letoutput=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");});}functionlogErrors(fileName: string){letallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(fileName)).concat(services.getSemanticDiagnostics(fileName));allDiagnostics.forEach(diagnostic=>{letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,"\n");if(diagnostic.file){let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);console.log(` Error ${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);}else{console.log(` Error: ${message}`);}});}}// Initialize files constituting the program as all .ts files in the current directoryconstcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(fileName=>fileName.length>=3&&fileName.substr(fileName.length-3,3)===".ts");// Start the watcherwatch(currentDirectoryFiles,{module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){letoptions=getDefaultOptions();// Parse the source textletsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,/*setParentPointers*/true);// Get the formatting edits on the input sourcesletedits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.letruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textletresult=text;for(leti=edits.length-1;i>=0;i--){letchange=edits[i];lethead=result.slice(0,change.span.start);lettail=result.slice(change.span.start+change.span.length)result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}}letcode="var a=function(v:number){return 0+1+2+3;\n}";letresult=format(code);console.log(result);

Transpiling a single file

Currently TypeScript exposes two functions for this purpose: transpileModule and transpile (which is deprecated). Note that regardless of the name, each one assumes that the input file is a module.

Here is the relevant signature of transpileModule:

exportinterfaceTranspileOptions{compilerOptions?: CompilerOptions;fileName?: string;reportDiagnostics?: boolean;moduleName?: string;renamedDependencies?: Map<string>;}exportinterfaceTranspileOutput{outputText: string;diagnostics?: Diagnostic[];sourceMapText?: string;}/* * This function will compile source text from 'input' argument using specified compiler options. * If not options are provided - it will use a set of default compiler options. * Extra compiler options that will unconditionally be used by this function are: * - isolatedModules = true * - allowNonTsExtensions = true * - noLib = true * - noResolve = true */exportfunctiontranspileModule(input: string,transpileOptions: TranspileOptions): TranspileOutput

and here is the appropriate version of transpile:

exportfunctiontranspile(input: string,compilerOptions?: CompilerOptions,fileName?: string,diagnostics?: Diagnostic[],moduleName?: string): string;

Historical note: initially only transpile function existed, however it was pretty difficult to extend (i.e to add new input parameters or return some extra information like source maps) without breaking existing consumers. As a result transpile is currently considered deprecated and superseded by transpileModule.

varts=require("typescript");varcontent="import {f} from \"foo\"\n"+"export var x = f()";varcompilerOptions={module: ts.ModuleKind.System};varres1=ts.transpileModule(content,{compilerOptions: compilerOptions,moduleName: "myModule2"});console.log(res1.outputText);console.log("============")varres2=ts.transpile(content,compilerOptions,/*fileName*/undefined,/*diagnostics*/undefined,/*moduleName*/"myModule1");console.log(res2);

Usually TypeScript compiler uses file extension to determine if file should be parsed as '.tsx' or '.ts'. The same rule is applied during single file transpilation if the file name is provided. If the file name is omitted, then compiler will check if the jsx options is specified. If it is set and is not JsxEmit.None, then source text will be interpreted as '.tsx'.

Customizing module resolution

You can override the standard way the compiler uses to resolve modules by implementing optional method: CompilerHost.resolveModuleNames:

CompilerHost.resolveModuleNames(moduleNames: string[], containingFile: string): string[].

The method is given a list of module names in a file, and is expected to return an array of size moduleNames.length, each element of the array stores either:

  • an instance of ResolvedModule with non-empty property resolvedFileName - resolution for corresponding name from moduleNames array or
  • undefined if module name cannot be resolved.

You can invoke the standard module resolution process via calling resolveModuleName:

resolveModuleName(moduleName: string, containingFile: string, options: CompilerOptions, moduleResolutionHost: ModuleResolutionHost): ResolvedModuleNameWithFallbackLocations.

This function returns an object that stores result of module resolution (value of resolvedModule property) as well as list of file names that were considered candidates before making current decision.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*aspathfrom"path";functioncreateCompilerHost(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(),getCanonicalFileName: fileName=>ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),getNewLine: ()=>ts.sys.newLine,useCaseSensitiveFileNames: ()=>ts.sys.useCaseSensitiveFileNames,
fileExists,
readFile,
resolveModuleNames
}functionfileExists(fileName: string): boolean{returnts.sys.fileExists(fileName);}functionreadFile(fileName: string): string{returnts.sys.readFile(fileName);}functiongetSourceFile(fileName: string,languageVersion: ts.ScriptTarget,onError?: (message: string)=>void){constsourceText=ts.sys.readFile(fileName);returnsourceText!==undefined ? ts.createSourceFile(fileName,sourceText,languageVersion) : undefined;}functionresolveModuleNames(moduleNames: string[],containingFile: string): ts.ResolvedModule[]{returnmoduleNames.map(moduleName=>{// try to use standard resolutionletresult=ts.resolveModuleName(moduleName,containingFile,options,{fileExists, readFile});if(result.resolvedModule){returnresult.resolvedModule;}// check fallback locations, for simplicity assume that module at location should be represented by '.d.ts' filefor(constlocationofmoduleSearchLocations){constmodulePath=path.join(location,moduleName+".d.ts");if(fileExists(modulePath)){return{resolvedFileName: modulePath}}}returnundefined;});}}functioncompile(sourceFiles: string[],moduleSearchLocations: string[]): void{constoptions: ts.CompilerOptions={module: ts.ModuleKind.AMD,target: ts.ScriptTarget.ES5};consthost=createCompilerHost(options,moduleSearchLocations);constprogram=ts.createProgram(sourceFiles,options,host);/// do something with program...}

Using the Type Checker

In this example we will walk the AST and use the checker to serialize class information. We'll use the type checker to get symbol and type information, while grabbing JSDoc comments for exported classes, their constructors, and respective constructor parameters.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*asfsfrom"fs";interfaceDocEntry{name?: string,fileName?: string,documentation?: string,type?: string,constructors?: DocEntry[],parameters?: DocEntry[],returnType?: string};/** Generate documention for all classes in a set of .ts files */functiongenerateDocumentation(fileNames: string[],options: ts.CompilerOptions): void{// Build a program using the set of root file names in fileNamesletprogram=ts.createProgram(fileNames,options);// Get the checker, we will use it to find more about classesletchecker=program.getTypeChecker();letoutput: DocEntry[]=[];// Visit every sourceFile in the program for(constsourceFileofprogram.getSourceFiles()){// Walk the tree to search for classests.forEachChild(sourceFile,visit);}// print out the docfs.writeFileSync("classes.json",JSON.stringify(output,undefined,4));return;/** visit nodes finding exported classes */functionvisit(node: ts.Node){// Only consider exported nodesif(!isNodeExported(node)){return;}if(node.kind===ts.SyntaxKind.ClassDeclaration){// This is a top level class, get its symbolletsymbol=checker.getSymbolAtLocation((<ts.ClassDeclaration>node).name);output.push(serializeClass(symbol));// No need to walk any further, class expressions/inner declarations// cannot be exported}elseif(node.kind===ts.SyntaxKind.ModuleDeclaration){// This is a namespace, visit its childrents.forEachChild(node,visit);}}/** Serialize a symbol into a json object */functionserializeSymbol(symbol: ts.Symbol): DocEntry{return{name: symbol.getName(),documentation: ts.displayPartsToString(symbol.getDocumentationComment()),type: checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration))};}/** Serialize a class symbol infomration */functionserializeClass(symbol: ts.Symbol){letdetails=serializeSymbol(symbol);// Get the construct signaturesletconstructorType=checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration);details.constructors=constructorType.getConstructSignatures().map(serializeSignature);returndetails;}/** Serialize a signature (call or construct) */functionserializeSignature(signature: ts.Signature){return{parameters: signature.parameters.map(serializeSymbol),returnType: checker.typeToString(signature.getReturnType()),documentation: ts.displayPartsToString(signature.getDocumentationComment())};}/** True if this is visible outside this file, false otherwise */functionisNodeExported(node: ts.Node): boolean{return(node.flags&ts.NodeFlags.Export)!==0||(node.parent&&node.parent.kind===ts.SyntaxKind.SourceFile);}}generateDocumentation(process.argv.slice(2),{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

to try this:

tsc docGenerator.ts --m commonjs node docGenerator.js test.ts

Passing an input like:

/**  * Documentation for C  */classC{/**  * constructor documentation * @param a my parameter documentation * @param b another parameter documentation */constructor(a: string,b: C){}}

We should get output like:

[
{
"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"
}
]
}
]

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Using the Compiler API

Daniel Rosenwasser edited this page Aug 18, 2016 · 52 revisions

##Disclaimer Keep in mind that this is not yet a stable API - we’re releasing this as version 0.5, and things will be changing over time. As a first iteration, there will be a few rough edges. We encourage any and all feedback from the community to improve the API. To allow users to transition between future releases, we will be documenting any API Breaking Changes per new release.

Getting set up

First you'll need to install TypeScript >=1.6 from npm.

For API Samples compatible with TypeScript == 1.4 please see Using the Compiler API (TypeScript 1.4)

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that will take a list of TypeScript files and compile down to their corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";functioncompile(fileNames: string[],options: ts.CompilerOptions): void{letprogram=ts.createProgram(fileNames,options);letemitResult=program.emit();letallDiagnostics=ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);allDiagnostics.forEach(diagnostic=>{let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,'\n');console.log(`${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);});letexitCode=emitResult.emitSkipped ? 1 : 0;console.log(`Process exiting with code '${exitCode}'.`);process.exit(exitCode);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may want to just get the corresponding JavaScript output given TypeScript sources. For this you can use ts.transpileModule to get a string => string transformation in two lines.

import*astsfrom"typescript";constsource="let x: string = 'string'";letresult=ts.transpileModule(source,{compilerOptions: {module: ts.ModuleKind.CommonJS}});console.log(JSON.stringify(result));

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />import{readFileSync}from"fs";import*astsfrom"typescript";exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
letifStatement=(<ts.IfStatement>node);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;casets.SyntaxKind.BinaryExpression:
letop=(<ts.BinaryExpression>node).operatorToken.kind;if(op===ts.SyntaxKind.EqualsEqualsToken||op==ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){let{ line, character }=sourceFile.getLineAndCharacterOfPosition(node.getStart());console.log(`${sourceFile.fileName} (${line+1},${character+1}): ${message}`);}}constfileNames=process.argv.slice(2);fileNames.forEach(fileName=>{// Parse a fileletsourceFile=ts.createSourceFile(fileName,readFileSync(fileName).toString(),ts.ScriptTarget.ES6,/*setParentNodes */true);// delint itdelint(sourceFile);});

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and updates only the outputs of the files that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version allows the language service to track changes to files. isOpen tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />import*asfsfrom"fs";import*astsfrom"typescript";functionwatch(rootFileNames: string[],options: ts.CompilerOptions){constfiles: ts.Map<{version: number}>={};// initialize the list of filesrootFileNames.forEach(fileName=>{files[fileName]={version: 0};});// Create the language service host to allow the LS to communicate with the hostconstservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>rootFileNames,getScriptVersion: (fileName)=>files[fileName]&&files[fileName].version.toString(),getScriptSnapshot: (fileName)=>{if(!fs.existsSync(fileName)){returnundefined;}returnts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString());},getCurrentDirectory: ()=>process.cwd(),getCompilationSettings: ()=>options,getDefaultLibFileName: (options)=>ts.getDefaultLibFilePath(options),};// Create the language service filesconstservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesrootFileNames.forEach(fileName=>{// First time around, emit all filesemitFile(fileName);// Add a watch on the file to handle next changefs.watchFile(fileName,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}// Update the version to signal a change in the filefiles[fileName].version++;// write the changes to diskemitFile(fileName);});});functionemitFile(fileName: string){letoutput=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");});}functionlogErrors(fileName: string){letallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(fileName)).concat(services.getSemanticDiagnostics(fileName));allDiagnostics.forEach(diagnostic=>{letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,"\n");if(diagnostic.file){let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);console.log(` Error ${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);}else{console.log(` Error: ${message}`);}});}}// Initialize files constituting the program as all .ts files in the current directoryconstcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(fileName=>fileName.length>=3&&fileName.substr(fileName.length-3,3)===".ts");// Start the watcherwatch(currentDirectoryFiles,{module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){letoptions=getDefaultOptions();// Parse the source textletsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,/*setParentPointers*/true);// Get the formatting edits on the input sourcesletedits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.letruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textletresult=text;for(leti=edits.length-1;i>=0;i--){letchange=edits[i];lethead=result.slice(0,change.span.start);lettail=result.slice(change.span.start+change.span.length)result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}}letcode="var a=function(v:number){return 0+1+2+3;\n}";letresult=format(code);console.log(result);

Transpiling a single file

Currently TypeScript exposes two functions for this purpose: transpileModule and transpile (which is deprecated). Note that regardless of the name, each one assumes that the input file is a module.

Here is the relevant signature of transpileModule:

exportinterfaceTranspileOptions{compilerOptions?: CompilerOptions;fileName?: string;reportDiagnostics?: boolean;moduleName?: string;renamedDependencies?: Map<string>;}exportinterfaceTranspileOutput{outputText: string;diagnostics?: Diagnostic[];sourceMapText?: string;}/* * This function will compile source text from 'input' argument using specified compiler options. * If not options are provided - it will use a set of default compiler options. * Extra compiler options that will unconditionally be used by this function are: * - isolatedModules = true * - allowNonTsExtensions = true * - noLib = true * - noResolve = true */exportfunctiontranspileModule(input: string,transpileOptions: TranspileOptions): TranspileOutput

and here is the appropriate version of transpile:

exportfunctiontranspile(input: string,compilerOptions?: CompilerOptions,fileName?: string,diagnostics?: Diagnostic[],moduleName?: string): string;

Historical note: initially only transpile function existed, however it was pretty difficult to extend (i.e to add new input parameters or return some extra information like source maps) without breaking existing consumers. As a result transpile is currently considered deprecated and superseded by transpileModule.

varts=require("typescript");varcontent="import {f} from \"foo\"\n"+"export var x = f()";varcompilerOptions={module: ts.ModuleKind.System};varres1=ts.transpileModule(content,{compilerOptions: compilerOptions,moduleName: "myModule2"});console.log(res1.outputText);console.log("============")varres2=ts.transpile(content,compilerOptions,/*fileName*/undefined,/*diagnostics*/undefined,/*moduleName*/"myModule1");console.log(res2);

Usually TypeScript compiler uses file extension to determine if file should be parsed as '.tsx' or '.ts'. The same rule is applied during single file transpilation if the file name is provided. If the file name is omitted, then compiler will check if the jsx options is specified. If it is set and is not JsxEmit.None, then source text will be interpreted as '.tsx'.

Customizing module resolution

You can override the standard way the compiler uses to resolve modules by implementing optional method: CompilerHost.resolveModuleNames:

CompilerHost.resolveModuleNames(moduleNames: string[], containingFile: string): string[].

The method is given a list of module names in a file, and is expected to return an array of size moduleNames.length, each element of the array stores either:

  • an instance of ResolvedModule with non-empty property resolvedFileName - resolution for corresponding name from moduleNames array or
  • undefined if module name cannot be resolved.

You can invoke the standard module resolution process via calling resolveModuleName:

resolveModuleName(moduleName: string, containingFile: string, options: CompilerOptions, moduleResolutionHost: ModuleResolutionHost): ResolvedModuleNameWithFallbackLocations.

This function returns an object that stores result of module resolution (value of resolvedModule property) as well as list of file names that were considered candidates before making current decision.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*aspathfrom"path";functioncreateCompilerHost(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(),getCanonicalFileName: fileName=>ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),getNewLine: ()=>ts.sys.newLine,useCaseSensitiveFileNames: ()=>ts.sys.useCaseSensitiveFileNames,
fileExists,
readFile,
resolveModuleNames
}functionfileExists(fileName: string): boolean{returnts.sys.fileExists(fileName);}functionreadFile(fileName: string): string{returnts.sys.readFile(fileName);}functiongetSourceFile(fileName: string,languageVersion: ts.ScriptTarget,onError?: (message: string)=>void){constsourceText=ts.sys.readFile(fileName);returnsourceText!==undefined ? ts.createSourceFile(fileName,sourceText,languageVersion) : undefined;}functionresolveModuleNames(moduleNames: string[],containingFile: string): ts.ResolvedModule[]{returnmoduleNames.map(moduleName=>{// try to use standard resolutionletresult=ts.resolveModuleName(moduleName,containingFile,options,{fileExists, readFile});if(result.resolvedModule){returnresult.resolvedModule;}// check fallback locations, for simplicity assume that module at location should be represented by '.d.ts' filefor(constlocationofmoduleSearchLocations){constmodulePath=path.join(location,moduleName+".d.ts");if(fileExists(modulePath)){return{resolvedFileName: modulePath}}}returnundefined;});}}functioncompile(sourceFiles: string[],moduleSearchLocations: string[]): void{constoptions: ts.CompilerOptions={module: ts.ModuleKind.AMD,target: ts.ScriptTarget.ES5};consthost=createCompilerHost(options,moduleSearchLocations);constprogram=ts.createProgram(sourceFiles,options,host);/// do something with program...}

Using the Type Checker

In this example we will walk the AST and use the checker to serialize class information. We'll use the type checker to get symbol and type information, while grabbing JSDoc comments for exported classes, their constructors, and respective constructor parameters.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*asfsfrom"fs";interfaceDocEntry{name?: string,fileName?: string,documentation?: string,type?: string,constructors?: DocEntry[],parameters?: DocEntry[],returnType?: string};/** Generate documention for all classes in a set of .ts files */functiongenerateDocumentation(fileNames: string[],options: ts.CompilerOptions): void{// Build a program using the set of root file names in fileNamesletprogram=ts.createProgram(fileNames,options);// Get the checker, we will use it to find more about classesletchecker=program.getTypeChecker();letoutput: DocEntry[]=[];// Visit every sourceFile in the program for(constsourceFileofprogram.getSourceFiles()){// Walk the tree to search for classests.forEachChild(sourceFile,visit);}// print out the docfs.writeFileSync("classes.json",JSON.stringify(output,undefined,4));return;/** visit nodes finding exported classes */functionvisit(node: ts.Node){// Only consider exported nodesif(!isNodeExported(node)){return;}if(node.kind===ts.SyntaxKind.ClassDeclaration){// This is a top level class, get its symbolletsymbol=checker.getSymbolAtLocation((<ts.ClassDeclaration>node).name);output.push(serializeClass(symbol));// No need to walk any further, class expressions/inner declarations// cannot be exported}elseif(node.kind===ts.SyntaxKind.ModuleDeclaration){// This is a namespace, visit its childrents.forEachChild(node,visit);}}/** Serialize a symbol into a json object */functionserializeSymbol(symbol: ts.Symbol): DocEntry{return{name: symbol.getName(),documentation: ts.displayPartsToString(symbol.getDocumentationComment()),type: checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration))};}/** Serialize a class symbol infomration */functionserializeClass(symbol: ts.Symbol){letdetails=serializeSymbol(symbol);// Get the construct signaturesletconstructorType=checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration);details.constructors=constructorType.getConstructSignatures().map(serializeSignature);returndetails;}/** Serialize a signature (call or construct) */functionserializeSignature(signature: ts.Signature){return{parameters: signature.parameters.map(serializeSymbol),returnType: checker.typeToString(signature.getReturnType()),documentation: ts.displayPartsToString(signature.getDocumentationComment())};}/** True if this is visible outside this file, false otherwise */functionisNodeExported(node: ts.Node): boolean{return(node.flags&ts.NodeFlags.Export)!==0||(node.parent&&node.parent.kind===ts.SyntaxKind.SourceFile);}}generateDocumentation(process.argv.slice(2),{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

to try this:

tsc docGenerator.ts --m commonjs node docGenerator.js test.ts

Passing an input like:

/**  * Documentation for C  */classC{/**  * constructor documentation * @param a my parameter documentation * @param b another parameter documentation */constructor(a: string,b: C){}}

We should get output like:

[
{
"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"
}
]
}
]

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Using the Compiler API

Daniel Rosenwasser edited this page Aug 18, 2016 · 52 revisions

##Disclaimer Keep in mind that this is not yet a stable API - we’re releasing this as version 0.5, and things will be changing over time. As a first iteration, there will be a few rough edges. We encourage any and all feedback from the community to improve the API. To allow users to transition between future releases, we will be documenting any API Breaking Changes per new release.

Getting set up

First you'll need to install TypeScript >=1.6 from npm.

For API Samples compatible with TypeScript == 1.4 please see Using the Compiler API (TypeScript 1.4)

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that will take a list of TypeScript files and compile down to their corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";functioncompile(fileNames: string[],options: ts.CompilerOptions): void{letprogram=ts.createProgram(fileNames,options);letemitResult=program.emit();letallDiagnostics=ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);allDiagnostics.forEach(diagnostic=>{let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,'\n');console.log(`${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);});letexitCode=emitResult.emitSkipped ? 1 : 0;console.log(`Process exiting with code '${exitCode}'.`);process.exit(exitCode);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may want to just get the corresponding JavaScript output given TypeScript sources. For this you can use ts.transpileModule to get a string => string transformation in two lines.

import*astsfrom"typescript";constsource="let x: string = 'string'";letresult=ts.transpileModule(source,{compilerOptions: {module: ts.ModuleKind.CommonJS}});console.log(JSON.stringify(result));

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />import{readFileSync}from"fs";import*astsfrom"typescript";exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
letifStatement=(<ts.IfStatement>node);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;casets.SyntaxKind.BinaryExpression:
letop=(<ts.BinaryExpression>node).operatorToken.kind;if(op===ts.SyntaxKind.EqualsEqualsToken||op==ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){let{ line, character }=sourceFile.getLineAndCharacterOfPosition(node.getStart());console.log(`${sourceFile.fileName} (${line+1},${character+1}): ${message}`);}}constfileNames=process.argv.slice(2);fileNames.forEach(fileName=>{// Parse a fileletsourceFile=ts.createSourceFile(fileName,readFileSync(fileName).toString(),ts.ScriptTarget.ES6,/*setParentNodes */true);// delint itdelint(sourceFile);});

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and updates only the outputs of the files that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version allows the language service to track changes to files. isOpen tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />import*asfsfrom"fs";import*astsfrom"typescript";functionwatch(rootFileNames: string[],options: ts.CompilerOptions){constfiles: ts.Map<{version: number}>={};// initialize the list of filesrootFileNames.forEach(fileName=>{files[fileName]={version: 0};});// Create the language service host to allow the LS to communicate with the hostconstservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>rootFileNames,getScriptVersion: (fileName)=>files[fileName]&&files[fileName].version.toString(),getScriptSnapshot: (fileName)=>{if(!fs.existsSync(fileName)){returnundefined;}returnts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString());},getCurrentDirectory: ()=>process.cwd(),getCompilationSettings: ()=>options,getDefaultLibFileName: (options)=>ts.getDefaultLibFilePath(options),};// Create the language service filesconstservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesrootFileNames.forEach(fileName=>{// First time around, emit all filesemitFile(fileName);// Add a watch on the file to handle next changefs.watchFile(fileName,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}// Update the version to signal a change in the filefiles[fileName].version++;// write the changes to diskemitFile(fileName);});});functionemitFile(fileName: string){letoutput=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");});}functionlogErrors(fileName: string){letallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(fileName)).concat(services.getSemanticDiagnostics(fileName));allDiagnostics.forEach(diagnostic=>{letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,"\n");if(diagnostic.file){let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);console.log(` Error ${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);}else{console.log(` Error: ${message}`);}});}}// Initialize files constituting the program as all .ts files in the current directoryconstcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(fileName=>fileName.length>=3&&fileName.substr(fileName.length-3,3)===".ts");// Start the watcherwatch(currentDirectoryFiles,{module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){letoptions=getDefaultOptions();// Parse the source textletsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,/*setParentPointers*/true);// Get the formatting edits on the input sourcesletedits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.letruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textletresult=text;for(leti=edits.length-1;i>=0;i--){letchange=edits[i];lethead=result.slice(0,change.span.start);lettail=result.slice(change.span.start+change.span.length)result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}}letcode="var a=function(v:number){return 0+1+2+3;\n}";letresult=format(code);console.log(result);

Transpiling a single file

Currently TypeScript exposes two functions for this purpose: transpileModule and transpile (which is deprecated). Note that regardless of the name, each one assumes that the input file is a module.

Here is the relevant signature of transpileModule:

exportinterfaceTranspileOptions{compilerOptions?: CompilerOptions;fileName?: string;reportDiagnostics?: boolean;moduleName?: string;renamedDependencies?: Map<string>;}exportinterfaceTranspileOutput{outputText: string;diagnostics?: Diagnostic[];sourceMapText?: string;}/* * This function will compile source text from 'input' argument using specified compiler options. * If not options are provided - it will use a set of default compiler options. * Extra compiler options that will unconditionally be used by this function are: * - isolatedModules = true * - allowNonTsExtensions = true * - noLib = true * - noResolve = true */exportfunctiontranspileModule(input: string,transpileOptions: TranspileOptions): TranspileOutput

and here is the appropriate version of transpile:

exportfunctiontranspile(input: string,compilerOptions?: CompilerOptions,fileName?: string,diagnostics?: Diagnostic[],moduleName?: string): string;

Historical note: initially only transpile function existed, however it was pretty difficult to extend (i.e to add new input parameters or return some extra information like source maps) without breaking existing consumers. As a result transpile is currently considered deprecated and superseded by transpileModule.

varts=require("typescript");varcontent="import {f} from \"foo\"\n"+"export var x = f()";varcompilerOptions={module: ts.ModuleKind.System};varres1=ts.transpileModule(content,{compilerOptions: compilerOptions,moduleName: "myModule2"});console.log(res1.outputText);console.log("============")varres2=ts.transpile(content,compilerOptions,/*fileName*/undefined,/*diagnostics*/undefined,/*moduleName*/"myModule1");console.log(res2);

Usually TypeScript compiler uses file extension to determine if file should be parsed as '.tsx' or '.ts'. The same rule is applied during single file transpilation if the file name is provided. If the file name is omitted, then compiler will check if the jsx options is specified. If it is set and is not JsxEmit.None, then source text will be interpreted as '.tsx'.

Customizing module resolution

You can override the standard way the compiler uses to resolve modules by implementing optional method: CompilerHost.resolveModuleNames:

CompilerHost.resolveModuleNames(moduleNames: string[], containingFile: string): string[].

The method is given a list of module names in a file, and is expected to return an array of size moduleNames.length, each element of the array stores either:

  • an instance of ResolvedModule with non-empty property resolvedFileName - resolution for corresponding name from moduleNames array or
  • undefined if module name cannot be resolved.

You can invoke the standard module resolution process via calling resolveModuleName:

resolveModuleName(moduleName: string, containingFile: string, options: CompilerOptions, moduleResolutionHost: ModuleResolutionHost): ResolvedModuleNameWithFallbackLocations.

This function returns an object that stores result of module resolution (value of resolvedModule property) as well as list of file names that were considered candidates before making current decision.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*aspathfrom"path";functioncreateCompilerHost(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(),getCanonicalFileName: fileName=>ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),getNewLine: ()=>ts.sys.newLine,useCaseSensitiveFileNames: ()=>ts.sys.useCaseSensitiveFileNames,
fileExists,
readFile,
resolveModuleNames
}functionfileExists(fileName: string): boolean{returnts.sys.fileExists(fileName);}functionreadFile(fileName: string): string{returnts.sys.readFile(fileName);}functiongetSourceFile(fileName: string,languageVersion: ts.ScriptTarget,onError?: (message: string)=>void){constsourceText=ts.sys.readFile(fileName);returnsourceText!==undefined ? ts.createSourceFile(fileName,sourceText,languageVersion) : undefined;}functionresolveModuleNames(moduleNames: string[],containingFile: string): ts.ResolvedModule[]{returnmoduleNames.map(moduleName=>{// try to use standard resolutionletresult=ts.resolveModuleName(moduleName,containingFile,options,{fileExists, readFile});if(result.resolvedModule){returnresult.resolvedModule;}// check fallback locations, for simplicity assume that module at location should be represented by '.d.ts' filefor(constlocationofmoduleSearchLocations){constmodulePath=path.join(location,moduleName+".d.ts");if(fileExists(modulePath)){return{resolvedFileName: modulePath}}}returnundefined;});}}functioncompile(sourceFiles: string[],moduleSearchLocations: string[]): void{constoptions: ts.CompilerOptions={module: ts.ModuleKind.AMD,target: ts.ScriptTarget.ES5};consthost=createCompilerHost(options,moduleSearchLocations);constprogram=ts.createProgram(sourceFiles,options,host);/// do something with program...}

Using the Type Checker

In this example we will walk the AST and use the checker to serialize class information. We'll use the type checker to get symbol and type information, while grabbing JSDoc comments for exported classes, their constructors, and respective constructor parameters.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*asfsfrom"fs";interfaceDocEntry{name?: string,fileName?: string,documentation?: string,type?: string,constructors?: DocEntry[],parameters?: DocEntry[],returnType?: string};/** Generate documention for all classes in a set of .ts files */functiongenerateDocumentation(fileNames: string[],options: ts.CompilerOptions): void{// Build a program using the set of root file names in fileNamesletprogram=ts.createProgram(fileNames,options);// Get the checker, we will use it to find more about classesletchecker=program.getTypeChecker();letoutput: DocEntry[]=[];// Visit every sourceFile in the program for(constsourceFileofprogram.getSourceFiles()){// Walk the tree to search for classests.forEachChild(sourceFile,visit);}// print out the docfs.writeFileSync("classes.json",JSON.stringify(output,undefined,4));return;/** visit nodes finding exported classes */functionvisit(node: ts.Node){// Only consider exported nodesif(!isNodeExported(node)){return;}if(node.kind===ts.SyntaxKind.ClassDeclaration){// This is a top level class, get its symbolletsymbol=checker.getSymbolAtLocation((<ts.ClassDeclaration>node).name);output.push(serializeClass(symbol));// No need to walk any further, class expressions/inner declarations// cannot be exported}elseif(node.kind===ts.SyntaxKind.ModuleDeclaration){// This is a namespace, visit its childrents.forEachChild(node,visit);}}/** Serialize a symbol into a json object */functionserializeSymbol(symbol: ts.Symbol): DocEntry{return{name: symbol.getName(),documentation: ts.displayPartsToString(symbol.getDocumentationComment()),type: checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration))};}/** Serialize a class symbol infomration */functionserializeClass(symbol: ts.Symbol){letdetails=serializeSymbol(symbol);// Get the construct signaturesletconstructorType=checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration);details.constructors=constructorType.getConstructSignatures().map(serializeSignature);returndetails;}/** Serialize a signature (call or construct) */functionserializeSignature(signature: ts.Signature){return{parameters: signature.parameters.map(serializeSymbol),returnType: checker.typeToString(signature.getReturnType()),documentation: ts.displayPartsToString(signature.getDocumentationComment())};}/** True if this is visible outside this file, false otherwise */functionisNodeExported(node: ts.Node): boolean{return(node.flags&ts.NodeFlags.Export)!==0||(node.parent&&node.parent.kind===ts.SyntaxKind.SourceFile);}}generateDocumentation(process.argv.slice(2),{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

to try this:

tsc docGenerator.ts --m commonjs node docGenerator.js test.ts

Passing an input like:

/**  * Documentation for C  */classC{/**  * constructor documentation * @param a my parameter documentation * @param b another parameter documentation */constructor(a: string,b: C){}}

We should get output like:

[
{
"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"
}
]
}
]

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Using the Compiler API

Daniel Rosenwasser edited this page Aug 18, 2016 · 52 revisions

##Disclaimer Keep in mind that this is not yet a stable API - we’re releasing this as version 0.5, and things will be changing over time. As a first iteration, there will be a few rough edges. We encourage any and all feedback from the community to improve the API. To allow users to transition between future releases, we will be documenting any API Breaking Changes per new release.

Getting set up

First you'll need to install TypeScript >=1.6 from npm.

For API Samples compatible with TypeScript == 1.4 please see Using the Compiler API (TypeScript 1.4)

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that will take a list of TypeScript files and compile down to their corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";functioncompile(fileNames: string[],options: ts.CompilerOptions): void{letprogram=ts.createProgram(fileNames,options);letemitResult=program.emit();letallDiagnostics=ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);allDiagnostics.forEach(diagnostic=>{let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,'\n');console.log(`${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);});letexitCode=emitResult.emitSkipped ? 1 : 0;console.log(`Process exiting with code '${exitCode}'.`);process.exit(exitCode);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may want to just get the corresponding JavaScript output given TypeScript sources. For this you can use ts.transpileModule to get a string => string transformation in two lines.

import*astsfrom"typescript";constsource="let x: string = 'string'";letresult=ts.transpileModule(source,{compilerOptions: {module: ts.ModuleKind.CommonJS}});console.log(JSON.stringify(result));

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />import{readFileSync}from"fs";import*astsfrom"typescript";exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
letifStatement=(<ts.IfStatement>node);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;casets.SyntaxKind.BinaryExpression:
letop=(<ts.BinaryExpression>node).operatorToken.kind;if(op===ts.SyntaxKind.EqualsEqualsToken||op==ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){let{ line, character }=sourceFile.getLineAndCharacterOfPosition(node.getStart());console.log(`${sourceFile.fileName} (${line+1},${character+1}): ${message}`);}}constfileNames=process.argv.slice(2);fileNames.forEach(fileName=>{// Parse a fileletsourceFile=ts.createSourceFile(fileName,readFileSync(fileName).toString(),ts.ScriptTarget.ES6,/*setParentNodes */true);// delint itdelint(sourceFile);});

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and updates only the outputs of the files that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version allows the language service to track changes to files. isOpen tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />import*asfsfrom"fs";import*astsfrom"typescript";functionwatch(rootFileNames: string[],options: ts.CompilerOptions){constfiles: ts.Map<{version: number}>={};// initialize the list of filesrootFileNames.forEach(fileName=>{files[fileName]={version: 0};});// Create the language service host to allow the LS to communicate with the hostconstservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>rootFileNames,getScriptVersion: (fileName)=>files[fileName]&&files[fileName].version.toString(),getScriptSnapshot: (fileName)=>{if(!fs.existsSync(fileName)){returnundefined;}returnts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString());},getCurrentDirectory: ()=>process.cwd(),getCompilationSettings: ()=>options,getDefaultLibFileName: (options)=>ts.getDefaultLibFilePath(options),};// Create the language service filesconstservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesrootFileNames.forEach(fileName=>{// First time around, emit all filesemitFile(fileName);// Add a watch on the file to handle next changefs.watchFile(fileName,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}// Update the version to signal a change in the filefiles[fileName].version++;// write the changes to diskemitFile(fileName);});});functionemitFile(fileName: string){letoutput=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");});}functionlogErrors(fileName: string){letallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(fileName)).concat(services.getSemanticDiagnostics(fileName));allDiagnostics.forEach(diagnostic=>{letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,"\n");if(diagnostic.file){let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);console.log(` Error ${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);}else{console.log(` Error: ${message}`);}});}}// Initialize files constituting the program as all .ts files in the current directoryconstcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(fileName=>fileName.length>=3&&fileName.substr(fileName.length-3,3)===".ts");// Start the watcherwatch(currentDirectoryFiles,{module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){letoptions=getDefaultOptions();// Parse the source textletsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,/*setParentPointers*/true);// Get the formatting edits on the input sourcesletedits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.letruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textletresult=text;for(leti=edits.length-1;i>=0;i--){letchange=edits[i];lethead=result.slice(0,change.span.start);lettail=result.slice(change.span.start+change.span.length)result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}}letcode="var a=function(v:number){return 0+1+2+3;\n}";letresult=format(code);console.log(result);

Transpiling a single file

Currently TypeScript exposes two functions for this purpose: transpileModule and transpile (which is deprecated). Note that regardless of the name, each one assumes that the input file is a module.

Here is the relevant signature of transpileModule:

exportinterfaceTranspileOptions{compilerOptions?: CompilerOptions;fileName?: string;reportDiagnostics?: boolean;moduleName?: string;renamedDependencies?: Map<string>;}exportinterfaceTranspileOutput{outputText: string;diagnostics?: Diagnostic[];sourceMapText?: string;}/* * This function will compile source text from 'input' argument using specified compiler options. * If not options are provided - it will use a set of default compiler options. * Extra compiler options that will unconditionally be used by this function are: * - isolatedModules = true * - allowNonTsExtensions = true * - noLib = true * - noResolve = true */exportfunctiontranspileModule(input: string,transpileOptions: TranspileOptions): TranspileOutput

and here is the appropriate version of transpile:

exportfunctiontranspile(input: string,compilerOptions?: CompilerOptions,fileName?: string,diagnostics?: Diagnostic[],moduleName?: string): string;

Historical note: initially only transpile function existed, however it was pretty difficult to extend (i.e to add new input parameters or return some extra information like source maps) without breaking existing consumers. As a result transpile is currently considered deprecated and superseded by transpileModule.

varts=require("typescript");varcontent="import {f} from \"foo\"\n"+"export var x = f()";varcompilerOptions={module: ts.ModuleKind.System};varres1=ts.transpileModule(content,{compilerOptions: compilerOptions,moduleName: "myModule2"});console.log(res1.outputText);console.log("============")varres2=ts.transpile(content,compilerOptions,/*fileName*/undefined,/*diagnostics*/undefined,/*moduleName*/"myModule1");console.log(res2);

Usually TypeScript compiler uses file extension to determine if file should be parsed as '.tsx' or '.ts'. The same rule is applied during single file transpilation if the file name is provided. If the file name is omitted, then compiler will check if the jsx options is specified. If it is set and is not JsxEmit.None, then source text will be interpreted as '.tsx'.

Customizing module resolution

You can override the standard way the compiler uses to resolve modules by implementing optional method: CompilerHost.resolveModuleNames:

CompilerHost.resolveModuleNames(moduleNames: string[], containingFile: string): string[].

The method is given a list of module names in a file, and is expected to return an array of size moduleNames.length, each element of the array stores either:

  • an instance of ResolvedModule with non-empty property resolvedFileName - resolution for corresponding name from moduleNames array or
  • undefined if module name cannot be resolved.

You can invoke the standard module resolution process via calling resolveModuleName:

resolveModuleName(moduleName: string, containingFile: string, options: CompilerOptions, moduleResolutionHost: ModuleResolutionHost): ResolvedModuleNameWithFallbackLocations.

This function returns an object that stores result of module resolution (value of resolvedModule property) as well as list of file names that were considered candidates before making current decision.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*aspathfrom"path";functioncreateCompilerHost(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(),getCanonicalFileName: fileName=>ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),getNewLine: ()=>ts.sys.newLine,useCaseSensitiveFileNames: ()=>ts.sys.useCaseSensitiveFileNames,
fileExists,
readFile,
resolveModuleNames
}functionfileExists(fileName: string): boolean{returnts.sys.fileExists(fileName);}functionreadFile(fileName: string): string{returnts.sys.readFile(fileName);}functiongetSourceFile(fileName: string,languageVersion: ts.ScriptTarget,onError?: (message: string)=>void){constsourceText=ts.sys.readFile(fileName);returnsourceText!==undefined ? ts.createSourceFile(fileName,sourceText,languageVersion) : undefined;}functionresolveModuleNames(moduleNames: string[],containingFile: string): ts.ResolvedModule[]{returnmoduleNames.map(moduleName=>{// try to use standard resolutionletresult=ts.resolveModuleName(moduleName,containingFile,options,{fileExists, readFile});if(result.resolvedModule){returnresult.resolvedModule;}// check fallback locations, for simplicity assume that module at location should be represented by '.d.ts' filefor(constlocationofmoduleSearchLocations){constmodulePath=path.join(location,moduleName+".d.ts");if(fileExists(modulePath)){return{resolvedFileName: modulePath}}}returnundefined;});}}functioncompile(sourceFiles: string[],moduleSearchLocations: string[]): void{constoptions: ts.CompilerOptions={module: ts.ModuleKind.AMD,target: ts.ScriptTarget.ES5};consthost=createCompilerHost(options,moduleSearchLocations);constprogram=ts.createProgram(sourceFiles,options,host);/// do something with program...}

Using the Type Checker

In this example we will walk the AST and use the checker to serialize class information. We'll use the type checker to get symbol and type information, while grabbing JSDoc comments for exported classes, their constructors, and respective constructor parameters.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*asfsfrom"fs";interfaceDocEntry{name?: string,fileName?: string,documentation?: string,type?: string,constructors?: DocEntry[],parameters?: DocEntry[],returnType?: string};/** Generate documention for all classes in a set of .ts files */functiongenerateDocumentation(fileNames: string[],options: ts.CompilerOptions): void{// Build a program using the set of root file names in fileNamesletprogram=ts.createProgram(fileNames,options);// Get the checker, we will use it to find more about classesletchecker=program.getTypeChecker();letoutput: DocEntry[]=[];// Visit every sourceFile in the program for(constsourceFileofprogram.getSourceFiles()){// Walk the tree to search for classests.forEachChild(sourceFile,visit);}// print out the docfs.writeFileSync("classes.json",JSON.stringify(output,undefined,4));return;/** visit nodes finding exported classes */functionvisit(node: ts.Node){// Only consider exported nodesif(!isNodeExported(node)){return;}if(node.kind===ts.SyntaxKind.ClassDeclaration){// This is a top level class, get its symbolletsymbol=checker.getSymbolAtLocation((<ts.ClassDeclaration>node).name);output.push(serializeClass(symbol));// No need to walk any further, class expressions/inner declarations// cannot be exported}elseif(node.kind===ts.SyntaxKind.ModuleDeclaration){// This is a namespace, visit its childrents.forEachChild(node,visit);}}/** Serialize a symbol into a json object */functionserializeSymbol(symbol: ts.Symbol): DocEntry{return{name: symbol.getName(),documentation: ts.displayPartsToString(symbol.getDocumentationComment()),type: checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration))};}/** Serialize a class symbol infomration */functionserializeClass(symbol: ts.Symbol){letdetails=serializeSymbol(symbol);// Get the construct signaturesletconstructorType=checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration);details.constructors=constructorType.getConstructSignatures().map(serializeSignature);returndetails;}/** Serialize a signature (call or construct) */functionserializeSignature(signature: ts.Signature){return{parameters: signature.parameters.map(serializeSymbol),returnType: checker.typeToString(signature.getReturnType()),documentation: ts.displayPartsToString(signature.getDocumentationComment())};}/** True if this is visible outside this file, false otherwise */functionisNodeExported(node: ts.Node): boolean{return(node.flags&ts.NodeFlags.Export)!==0||(node.parent&&node.parent.kind===ts.SyntaxKind.SourceFile);}}generateDocumentation(process.argv.slice(2),{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

to try this:

tsc docGenerator.ts --m commonjs node docGenerator.js test.ts

Passing an input like:

/**  * Documentation for C  */classC{/**  * constructor documentation * @param a my parameter documentation * @param b another parameter documentation */constructor(a: string,b: C){}}

We should get output like:

[
{
"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"
}
]
}
]

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Using the Compiler API

Daniel Rosenwasser edited this page Aug 18, 2016 · 52 revisions

##Disclaimer Keep in mind that this is not yet a stable API - we’re releasing this as version 0.5, and things will be changing over time. As a first iteration, there will be a few rough edges. We encourage any and all feedback from the community to improve the API. To allow users to transition between future releases, we will be documenting any API Breaking Changes per new release.

Getting set up

First you'll need to install TypeScript >=1.6 from npm.

For API Samples compatible with TypeScript == 1.4 please see Using the Compiler API (TypeScript 1.4)

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that will take a list of TypeScript files and compile down to their corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";functioncompile(fileNames: string[],options: ts.CompilerOptions): void{letprogram=ts.createProgram(fileNames,options);letemitResult=program.emit();letallDiagnostics=ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);allDiagnostics.forEach(diagnostic=>{let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,'\n');console.log(`${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);});letexitCode=emitResult.emitSkipped ? 1 : 0;console.log(`Process exiting with code '${exitCode}'.`);process.exit(exitCode);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may want to just get the corresponding JavaScript output given TypeScript sources. For this you can use ts.transpileModule to get a string => string transformation in two lines.

import*astsfrom"typescript";constsource="let x: string = 'string'";letresult=ts.transpileModule(source,{compilerOptions: {module: ts.ModuleKind.CommonJS}});console.log(JSON.stringify(result));

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />import{readFileSync}from"fs";import*astsfrom"typescript";exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
letifStatement=(<ts.IfStatement>node);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;casets.SyntaxKind.BinaryExpression:
letop=(<ts.BinaryExpression>node).operatorToken.kind;if(op===ts.SyntaxKind.EqualsEqualsToken||op==ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){let{ line, character }=sourceFile.getLineAndCharacterOfPosition(node.getStart());console.log(`${sourceFile.fileName} (${line+1},${character+1}): ${message}`);}}constfileNames=process.argv.slice(2);fileNames.forEach(fileName=>{// Parse a fileletsourceFile=ts.createSourceFile(fileName,readFileSync(fileName).toString(),ts.ScriptTarget.ES6,/*setParentNodes */true);// delint itdelint(sourceFile);});

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and updates only the outputs of the files that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version allows the language service to track changes to files. isOpen tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />import*asfsfrom"fs";import*astsfrom"typescript";functionwatch(rootFileNames: string[],options: ts.CompilerOptions){constfiles: ts.Map<{version: number}>={};// initialize the list of filesrootFileNames.forEach(fileName=>{files[fileName]={version: 0};});// Create the language service host to allow the LS to communicate with the hostconstservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>rootFileNames,getScriptVersion: (fileName)=>files[fileName]&&files[fileName].version.toString(),getScriptSnapshot: (fileName)=>{if(!fs.existsSync(fileName)){returnundefined;}returnts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString());},getCurrentDirectory: ()=>process.cwd(),getCompilationSettings: ()=>options,getDefaultLibFileName: (options)=>ts.getDefaultLibFilePath(options),};// Create the language service filesconstservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesrootFileNames.forEach(fileName=>{// First time around, emit all filesemitFile(fileName);// Add a watch on the file to handle next changefs.watchFile(fileName,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}// Update the version to signal a change in the filefiles[fileName].version++;// write the changes to diskemitFile(fileName);});});functionemitFile(fileName: string){letoutput=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");});}functionlogErrors(fileName: string){letallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(fileName)).concat(services.getSemanticDiagnostics(fileName));allDiagnostics.forEach(diagnostic=>{letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,"\n");if(diagnostic.file){let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);console.log(` Error ${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);}else{console.log(` Error: ${message}`);}});}}// Initialize files constituting the program as all .ts files in the current directoryconstcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(fileName=>fileName.length>=3&&fileName.substr(fileName.length-3,3)===".ts");// Start the watcherwatch(currentDirectoryFiles,{module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){letoptions=getDefaultOptions();// Parse the source textletsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,/*setParentPointers*/true);// Get the formatting edits on the input sourcesletedits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.letruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textletresult=text;for(leti=edits.length-1;i>=0;i--){letchange=edits[i];lethead=result.slice(0,change.span.start);lettail=result.slice(change.span.start+change.span.length)result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}}letcode="var a=function(v:number){return 0+1+2+3;\n}";letresult=format(code);console.log(result);

Transpiling a single file

Currently TypeScript exposes two functions for this purpose: transpileModule and transpile (which is deprecated). Note that regardless of the name, each one assumes that the input file is a module.

Here is the relevant signature of transpileModule:

exportinterfaceTranspileOptions{compilerOptions?: CompilerOptions;fileName?: string;reportDiagnostics?: boolean;moduleName?: string;renamedDependencies?: Map<string>;}exportinterfaceTranspileOutput{outputText: string;diagnostics?: Diagnostic[];sourceMapText?: string;}/* * This function will compile source text from 'input' argument using specified compiler options. * If not options are provided - it will use a set of default compiler options. * Extra compiler options that will unconditionally be used by this function are: * - isolatedModules = true * - allowNonTsExtensions = true * - noLib = true * - noResolve = true */exportfunctiontranspileModule(input: string,transpileOptions: TranspileOptions): TranspileOutput

and here is the appropriate version of transpile:

exportfunctiontranspile(input: string,compilerOptions?: CompilerOptions,fileName?: string,diagnostics?: Diagnostic[],moduleName?: string): string;

Historical note: initially only transpile function existed, however it was pretty difficult to extend (i.e to add new input parameters or return some extra information like source maps) without breaking existing consumers. As a result transpile is currently considered deprecated and superseded by transpileModule.

varts=require("typescript");varcontent="import {f} from \"foo\"\n"+"export var x = f()";varcompilerOptions={module: ts.ModuleKind.System};varres1=ts.transpileModule(content,{compilerOptions: compilerOptions,moduleName: "myModule2"});console.log(res1.outputText);console.log("============")varres2=ts.transpile(content,compilerOptions,/*fileName*/undefined,/*diagnostics*/undefined,/*moduleName*/"myModule1");console.log(res2);

Usually TypeScript compiler uses file extension to determine if file should be parsed as '.tsx' or '.ts'. The same rule is applied during single file transpilation if the file name is provided. If the file name is omitted, then compiler will check if the jsx options is specified. If it is set and is not JsxEmit.None, then source text will be interpreted as '.tsx'.

Customizing module resolution

You can override the standard way the compiler uses to resolve modules by implementing optional method: CompilerHost.resolveModuleNames:

CompilerHost.resolveModuleNames(moduleNames: string[], containingFile: string): string[].

The method is given a list of module names in a file, and is expected to return an array of size moduleNames.length, each element of the array stores either:

  • an instance of ResolvedModule with non-empty property resolvedFileName - resolution for corresponding name from moduleNames array or
  • undefined if module name cannot be resolved.

You can invoke the standard module resolution process via calling resolveModuleName:

resolveModuleName(moduleName: string, containingFile: string, options: CompilerOptions, moduleResolutionHost: ModuleResolutionHost): ResolvedModuleNameWithFallbackLocations.

This function returns an object that stores result of module resolution (value of resolvedModule property) as well as list of file names that were considered candidates before making current decision.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*aspathfrom"path";functioncreateCompilerHost(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(),getCanonicalFileName: fileName=>ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),getNewLine: ()=>ts.sys.newLine,useCaseSensitiveFileNames: ()=>ts.sys.useCaseSensitiveFileNames,
fileExists,
readFile,
resolveModuleNames
}functionfileExists(fileName: string): boolean{returnts.sys.fileExists(fileName);}functionreadFile(fileName: string): string{returnts.sys.readFile(fileName);}functiongetSourceFile(fileName: string,languageVersion: ts.ScriptTarget,onError?: (message: string)=>void){constsourceText=ts.sys.readFile(fileName);returnsourceText!==undefined ? ts.createSourceFile(fileName,sourceText,languageVersion) : undefined;}functionresolveModuleNames(moduleNames: string[],containingFile: string): ts.ResolvedModule[]{returnmoduleNames.map(moduleName=>{// try to use standard resolutionletresult=ts.resolveModuleName(moduleName,containingFile,options,{fileExists, readFile});if(result.resolvedModule){returnresult.resolvedModule;}// check fallback locations, for simplicity assume that module at location should be represented by '.d.ts' filefor(constlocationofmoduleSearchLocations){constmodulePath=path.join(location,moduleName+".d.ts");if(fileExists(modulePath)){return{resolvedFileName: modulePath}}}returnundefined;});}}functioncompile(sourceFiles: string[],moduleSearchLocations: string[]): void{constoptions: ts.CompilerOptions={module: ts.ModuleKind.AMD,target: ts.ScriptTarget.ES5};consthost=createCompilerHost(options,moduleSearchLocations);constprogram=ts.createProgram(sourceFiles,options,host);/// do something with program...}

Using the Type Checker

In this example we will walk the AST and use the checker to serialize class information. We'll use the type checker to get symbol and type information, while grabbing JSDoc comments for exported classes, their constructors, and respective constructor parameters.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*asfsfrom"fs";interfaceDocEntry{name?: string,fileName?: string,documentation?: string,type?: string,constructors?: DocEntry[],parameters?: DocEntry[],returnType?: string};/** Generate documention for all classes in a set of .ts files */functiongenerateDocumentation(fileNames: string[],options: ts.CompilerOptions): void{// Build a program using the set of root file names in fileNamesletprogram=ts.createProgram(fileNames,options);// Get the checker, we will use it to find more about classesletchecker=program.getTypeChecker();letoutput: DocEntry[]=[];// Visit every sourceFile in the program for(constsourceFileofprogram.getSourceFiles()){// Walk the tree to search for classests.forEachChild(sourceFile,visit);}// print out the docfs.writeFileSync("classes.json",JSON.stringify(output,undefined,4));return;/** visit nodes finding exported classes */functionvisit(node: ts.Node){// Only consider exported nodesif(!isNodeExported(node)){return;}if(node.kind===ts.SyntaxKind.ClassDeclaration){// This is a top level class, get its symbolletsymbol=checker.getSymbolAtLocation((<ts.ClassDeclaration>node).name);output.push(serializeClass(symbol));// No need to walk any further, class expressions/inner declarations// cannot be exported}elseif(node.kind===ts.SyntaxKind.ModuleDeclaration){// This is a namespace, visit its childrents.forEachChild(node,visit);}}/** Serialize a symbol into a json object */functionserializeSymbol(symbol: ts.Symbol): DocEntry{return{name: symbol.getName(),documentation: ts.displayPartsToString(symbol.getDocumentationComment()),type: checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration))};}/** Serialize a class symbol infomration */functionserializeClass(symbol: ts.Symbol){letdetails=serializeSymbol(symbol);// Get the construct signaturesletconstructorType=checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration);details.constructors=constructorType.getConstructSignatures().map(serializeSignature);returndetails;}/** Serialize a signature (call or construct) */functionserializeSignature(signature: ts.Signature){return{parameters: signature.parameters.map(serializeSymbol),returnType: checker.typeToString(signature.getReturnType()),documentation: ts.displayPartsToString(signature.getDocumentationComment())};}/** True if this is visible outside this file, false otherwise */functionisNodeExported(node: ts.Node): boolean{return(node.flags&ts.NodeFlags.Export)!==0||(node.parent&&node.parent.kind===ts.SyntaxKind.SourceFile);}}generateDocumentation(process.argv.slice(2),{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

to try this:

tsc docGenerator.ts --m commonjs node docGenerator.js test.ts

Passing an input like:

/**  * Documentation for C  */classC{/**  * constructor documentation * @param a my parameter documentation * @param b another parameter documentation */constructor(a: string,b: C){}}

We should get output like:

[
{
"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"
}
]
}
]

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Using the Compiler API

Daniel Rosenwasser edited this page Aug 18, 2016 · 52 revisions

##Disclaimer Keep in mind that this is not yet a stable API - we’re releasing this as version 0.5, and things will be changing over time. As a first iteration, there will be a few rough edges. We encourage any and all feedback from the community to improve the API. To allow users to transition between future releases, we will be documenting any API Breaking Changes per new release.

Getting set up

First you'll need to install TypeScript >=1.6 from npm.

For API Samples compatible with TypeScript == 1.4 please see Using the Compiler API (TypeScript 1.4)

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that will take a list of TypeScript files and compile down to their corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";functioncompile(fileNames: string[],options: ts.CompilerOptions): void{letprogram=ts.createProgram(fileNames,options);letemitResult=program.emit();letallDiagnostics=ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);allDiagnostics.forEach(diagnostic=>{let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,'\n');console.log(`${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);});letexitCode=emitResult.emitSkipped ? 1 : 0;console.log(`Process exiting with code '${exitCode}'.`);process.exit(exitCode);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may want to just get the corresponding JavaScript output given TypeScript sources. For this you can use ts.transpileModule to get a string => string transformation in two lines.

import*astsfrom"typescript";constsource="let x: string = 'string'";letresult=ts.transpileModule(source,{compilerOptions: {module: ts.ModuleKind.CommonJS}});console.log(JSON.stringify(result));

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />import{readFileSync}from"fs";import*astsfrom"typescript";exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
letifStatement=(<ts.IfStatement>node);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;casets.SyntaxKind.BinaryExpression:
letop=(<ts.BinaryExpression>node).operatorToken.kind;if(op===ts.SyntaxKind.EqualsEqualsToken||op==ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){let{ line, character }=sourceFile.getLineAndCharacterOfPosition(node.getStart());console.log(`${sourceFile.fileName} (${line+1},${character+1}): ${message}`);}}constfileNames=process.argv.slice(2);fileNames.forEach(fileName=>{// Parse a fileletsourceFile=ts.createSourceFile(fileName,readFileSync(fileName).toString(),ts.ScriptTarget.ES6,/*setParentNodes */true);// delint itdelint(sourceFile);});

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and updates only the outputs of the files that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version allows the language service to track changes to files. isOpen tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />import*asfsfrom"fs";import*astsfrom"typescript";functionwatch(rootFileNames: string[],options: ts.CompilerOptions){constfiles: ts.Map<{version: number}>={};// initialize the list of filesrootFileNames.forEach(fileName=>{files[fileName]={version: 0};});// Create the language service host to allow the LS to communicate with the hostconstservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>rootFileNames,getScriptVersion: (fileName)=>files[fileName]&&files[fileName].version.toString(),getScriptSnapshot: (fileName)=>{if(!fs.existsSync(fileName)){returnundefined;}returnts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString());},getCurrentDirectory: ()=>process.cwd(),getCompilationSettings: ()=>options,getDefaultLibFileName: (options)=>ts.getDefaultLibFilePath(options),};// Create the language service filesconstservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesrootFileNames.forEach(fileName=>{// First time around, emit all filesemitFile(fileName);// Add a watch on the file to handle next changefs.watchFile(fileName,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}// Update the version to signal a change in the filefiles[fileName].version++;// write the changes to diskemitFile(fileName);});});functionemitFile(fileName: string){letoutput=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");});}functionlogErrors(fileName: string){letallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(fileName)).concat(services.getSemanticDiagnostics(fileName));allDiagnostics.forEach(diagnostic=>{letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,"\n");if(diagnostic.file){let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);console.log(` Error ${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);}else{console.log(` Error: ${message}`);}});}}// Initialize files constituting the program as all .ts files in the current directoryconstcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(fileName=>fileName.length>=3&&fileName.substr(fileName.length-3,3)===".ts");// Start the watcherwatch(currentDirectoryFiles,{module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){letoptions=getDefaultOptions();// Parse the source textletsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,/*setParentPointers*/true);// Get the formatting edits on the input sourcesletedits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.letruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textletresult=text;for(leti=edits.length-1;i>=0;i--){letchange=edits[i];lethead=result.slice(0,change.span.start);lettail=result.slice(change.span.start+change.span.length)result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}}letcode="var a=function(v:number){return 0+1+2+3;\n}";letresult=format(code);console.log(result);

Transpiling a single file

Currently TypeScript exposes two functions for this purpose: transpileModule and transpile (which is deprecated). Note that regardless of the name, each one assumes that the input file is a module.

Here is the relevant signature of transpileModule:

exportinterfaceTranspileOptions{compilerOptions?: CompilerOptions;fileName?: string;reportDiagnostics?: boolean;moduleName?: string;renamedDependencies?: Map<string>;}exportinterfaceTranspileOutput{outputText: string;diagnostics?: Diagnostic[];sourceMapText?: string;}/* * This function will compile source text from 'input' argument using specified compiler options. * If not options are provided - it will use a set of default compiler options. * Extra compiler options that will unconditionally be used by this function are: * - isolatedModules = true * - allowNonTsExtensions = true * - noLib = true * - noResolve = true */exportfunctiontranspileModule(input: string,transpileOptions: TranspileOptions): TranspileOutput

and here is the appropriate version of transpile:

exportfunctiontranspile(input: string,compilerOptions?: CompilerOptions,fileName?: string,diagnostics?: Diagnostic[],moduleName?: string): string;

Historical note: initially only transpile function existed, however it was pretty difficult to extend (i.e to add new input parameters or return some extra information like source maps) without breaking existing consumers. As a result transpile is currently considered deprecated and superseded by transpileModule.

varts=require("typescript");varcontent="import {f} from \"foo\"\n"+"export var x = f()";varcompilerOptions={module: ts.ModuleKind.System};varres1=ts.transpileModule(content,{compilerOptions: compilerOptions,moduleName: "myModule2"});console.log(res1.outputText);console.log("============")varres2=ts.transpile(content,compilerOptions,/*fileName*/undefined,/*diagnostics*/undefined,/*moduleName*/"myModule1");console.log(res2);

Usually TypeScript compiler uses file extension to determine if file should be parsed as '.tsx' or '.ts'. The same rule is applied during single file transpilation if the file name is provided. If the file name is omitted, then compiler will check if the jsx options is specified. If it is set and is not JsxEmit.None, then source text will be interpreted as '.tsx'.

Customizing module resolution

You can override the standard way the compiler uses to resolve modules by implementing optional method: CompilerHost.resolveModuleNames:

CompilerHost.resolveModuleNames(moduleNames: string[], containingFile: string): string[].

The method is given a list of module names in a file, and is expected to return an array of size moduleNames.length, each element of the array stores either:

  • an instance of ResolvedModule with non-empty property resolvedFileName - resolution for corresponding name from moduleNames array or
  • undefined if module name cannot be resolved.

You can invoke the standard module resolution process via calling resolveModuleName:

resolveModuleName(moduleName: string, containingFile: string, options: CompilerOptions, moduleResolutionHost: ModuleResolutionHost): ResolvedModuleNameWithFallbackLocations.

This function returns an object that stores result of module resolution (value of resolvedModule property) as well as list of file names that were considered candidates before making current decision.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*aspathfrom"path";functioncreateCompilerHost(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(),getCanonicalFileName: fileName=>ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),getNewLine: ()=>ts.sys.newLine,useCaseSensitiveFileNames: ()=>ts.sys.useCaseSensitiveFileNames,
fileExists,
readFile,
resolveModuleNames
}functionfileExists(fileName: string): boolean{returnts.sys.fileExists(fileName);}functionreadFile(fileName: string): string{returnts.sys.readFile(fileName);}functiongetSourceFile(fileName: string,languageVersion: ts.ScriptTarget,onError?: (message: string)=>void){constsourceText=ts.sys.readFile(fileName);returnsourceText!==undefined ? ts.createSourceFile(fileName,sourceText,languageVersion) : undefined;}functionresolveModuleNames(moduleNames: string[],containingFile: string): ts.ResolvedModule[]{returnmoduleNames.map(moduleName=>{// try to use standard resolutionletresult=ts.resolveModuleName(moduleName,containingFile,options,{fileExists, readFile});if(result.resolvedModule){returnresult.resolvedModule;}// check fallback locations, for simplicity assume that module at location should be represented by '.d.ts' filefor(constlocationofmoduleSearchLocations){constmodulePath=path.join(location,moduleName+".d.ts");if(fileExists(modulePath)){return{resolvedFileName: modulePath}}}returnundefined;});}}functioncompile(sourceFiles: string[],moduleSearchLocations: string[]): void{constoptions: ts.CompilerOptions={module: ts.ModuleKind.AMD,target: ts.ScriptTarget.ES5};consthost=createCompilerHost(options,moduleSearchLocations);constprogram=ts.createProgram(sourceFiles,options,host);/// do something with program...}

Using the Type Checker

In this example we will walk the AST and use the checker to serialize class information. We'll use the type checker to get symbol and type information, while grabbing JSDoc comments for exported classes, their constructors, and respective constructor parameters.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*asfsfrom"fs";interfaceDocEntry{name?: string,fileName?: string,documentation?: string,type?: string,constructors?: DocEntry[],parameters?: DocEntry[],returnType?: string};/** Generate documention for all classes in a set of .ts files */functiongenerateDocumentation(fileNames: string[],options: ts.CompilerOptions): void{// Build a program using the set of root file names in fileNamesletprogram=ts.createProgram(fileNames,options);// Get the checker, we will use it to find more about classesletchecker=program.getTypeChecker();letoutput: DocEntry[]=[];// Visit every sourceFile in the program for(constsourceFileofprogram.getSourceFiles()){// Walk the tree to search for classests.forEachChild(sourceFile,visit);}// print out the docfs.writeFileSync("classes.json",JSON.stringify(output,undefined,4));return;/** visit nodes finding exported classes */functionvisit(node: ts.Node){// Only consider exported nodesif(!isNodeExported(node)){return;}if(node.kind===ts.SyntaxKind.ClassDeclaration){// This is a top level class, get its symbolletsymbol=checker.getSymbolAtLocation((<ts.ClassDeclaration>node).name);output.push(serializeClass(symbol));// No need to walk any further, class expressions/inner declarations// cannot be exported}elseif(node.kind===ts.SyntaxKind.ModuleDeclaration){// This is a namespace, visit its childrents.forEachChild(node,visit);}}/** Serialize a symbol into a json object */functionserializeSymbol(symbol: ts.Symbol): DocEntry{return{name: symbol.getName(),documentation: ts.displayPartsToString(symbol.getDocumentationComment()),type: checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration))};}/** Serialize a class symbol infomration */functionserializeClass(symbol: ts.Symbol){letdetails=serializeSymbol(symbol);// Get the construct signaturesletconstructorType=checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration);details.constructors=constructorType.getConstructSignatures().map(serializeSignature);returndetails;}/** Serialize a signature (call or construct) */functionserializeSignature(signature: ts.Signature){return{parameters: signature.parameters.map(serializeSymbol),returnType: checker.typeToString(signature.getReturnType()),documentation: ts.displayPartsToString(signature.getDocumentationComment())};}/** True if this is visible outside this file, false otherwise */functionisNodeExported(node: ts.Node): boolean{return(node.flags&ts.NodeFlags.Export)!==0||(node.parent&&node.parent.kind===ts.SyntaxKind.SourceFile);}}generateDocumentation(process.argv.slice(2),{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

to try this:

tsc docGenerator.ts --m commonjs node docGenerator.js test.ts

Passing an input like:

/**  * Documentation for C  */classC{/**  * constructor documentation * @param a my parameter documentation * @param b another parameter documentation */constructor(a: string,b: C){}}

We should get output like:

[
{
"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"
}
]
}
]

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Using the Compiler API

Daniel Rosenwasser edited this page Aug 18, 2016 · 52 revisions

##Disclaimer Keep in mind that this is not yet a stable API - we’re releasing this as version 0.5, and things will be changing over time. As a first iteration, there will be a few rough edges. We encourage any and all feedback from the community to improve the API. To allow users to transition between future releases, we will be documenting any API Breaking Changes per new release.

Getting set up

First you'll need to install TypeScript >=1.6 from npm.

For API Samples compatible with TypeScript == 1.4 please see Using the Compiler API (TypeScript 1.4)

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that will take a list of TypeScript files and compile down to their corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";functioncompile(fileNames: string[],options: ts.CompilerOptions): void{letprogram=ts.createProgram(fileNames,options);letemitResult=program.emit();letallDiagnostics=ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);allDiagnostics.forEach(diagnostic=>{let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,'\n');console.log(`${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);});letexitCode=emitResult.emitSkipped ? 1 : 0;console.log(`Process exiting with code '${exitCode}'.`);process.exit(exitCode);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may want to just get the corresponding JavaScript output given TypeScript sources. For this you can use ts.transpileModule to get a string => string transformation in two lines.

import*astsfrom"typescript";constsource="let x: string = 'string'";letresult=ts.transpileModule(source,{compilerOptions: {module: ts.ModuleKind.CommonJS}});console.log(JSON.stringify(result));

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />import{readFileSync}from"fs";import*astsfrom"typescript";exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
letifStatement=(<ts.IfStatement>node);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;casets.SyntaxKind.BinaryExpression:
letop=(<ts.BinaryExpression>node).operatorToken.kind;if(op===ts.SyntaxKind.EqualsEqualsToken||op==ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){let{ line, character }=sourceFile.getLineAndCharacterOfPosition(node.getStart());console.log(`${sourceFile.fileName} (${line+1},${character+1}): ${message}`);}}constfileNames=process.argv.slice(2);fileNames.forEach(fileName=>{// Parse a fileletsourceFile=ts.createSourceFile(fileName,readFileSync(fileName).toString(),ts.ScriptTarget.ES6,/*setParentNodes */true);// delint itdelint(sourceFile);});

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and updates only the outputs of the files that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version allows the language service to track changes to files. isOpen tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />import*asfsfrom"fs";import*astsfrom"typescript";functionwatch(rootFileNames: string[],options: ts.CompilerOptions){constfiles: ts.Map<{version: number}>={};// initialize the list of filesrootFileNames.forEach(fileName=>{files[fileName]={version: 0};});// Create the language service host to allow the LS to communicate with the hostconstservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>rootFileNames,getScriptVersion: (fileName)=>files[fileName]&&files[fileName].version.toString(),getScriptSnapshot: (fileName)=>{if(!fs.existsSync(fileName)){returnundefined;}returnts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString());},getCurrentDirectory: ()=>process.cwd(),getCompilationSettings: ()=>options,getDefaultLibFileName: (options)=>ts.getDefaultLibFilePath(options),};// Create the language service filesconstservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesrootFileNames.forEach(fileName=>{// First time around, emit all filesemitFile(fileName);// Add a watch on the file to handle next changefs.watchFile(fileName,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}// Update the version to signal a change in the filefiles[fileName].version++;// write the changes to diskemitFile(fileName);});});functionemitFile(fileName: string){letoutput=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");});}functionlogErrors(fileName: string){letallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(fileName)).concat(services.getSemanticDiagnostics(fileName));allDiagnostics.forEach(diagnostic=>{letmessage=ts.flattenDiagnosticMessageText(diagnostic.messageText,"\n");if(diagnostic.file){let{ line, character }=diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);console.log(` Error ${diagnostic.file.fileName} (${line+1},${character+1}): ${message}`);}else{console.log(` Error: ${message}`);}});}}// Initialize files constituting the program as all .ts files in the current directoryconstcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(fileName=>fileName.length>=3&&fileName.substr(fileName.length-3,3)===".ts");// Start the watcherwatch(currentDirectoryFiles,{module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){letoptions=getDefaultOptions();// Parse the source textletsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,/*setParentPointers*/true);// Get the formatting edits on the input sourcesletedits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.letruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textletresult=text;for(leti=edits.length-1;i>=0;i--){letchange=edits[i];lethead=result.slice(0,change.span.start);lettail=result.slice(change.span.start+change.span.length)result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}}letcode="var a=function(v:number){return 0+1+2+3;\n}";letresult=format(code);console.log(result);

Transpiling a single file

Currently TypeScript exposes two functions for this purpose: transpileModule and transpile (which is deprecated). Note that regardless of the name, each one assumes that the input file is a module.

Here is the relevant signature of transpileModule:

exportinterfaceTranspileOptions{compilerOptions?: CompilerOptions;fileName?: string;reportDiagnostics?: boolean;moduleName?: string;renamedDependencies?: Map<string>;}exportinterfaceTranspileOutput{outputText: string;diagnostics?: Diagnostic[];sourceMapText?: string;}/* * This function will compile source text from 'input' argument using specified compiler options. * If not options are provided - it will use a set of default compiler options. * Extra compiler options that will unconditionally be used by this function are: * - isolatedModules = true * - allowNonTsExtensions = true * - noLib = true * - noResolve = true */exportfunctiontranspileModule(input: string,transpileOptions: TranspileOptions): TranspileOutput

and here is the appropriate version of transpile:

exportfunctiontranspile(input: string,compilerOptions?: CompilerOptions,fileName?: string,diagnostics?: Diagnostic[],moduleName?: string): string;

Historical note: initially only transpile function existed, however it was pretty difficult to extend (i.e to add new input parameters or return some extra information like source maps) without breaking existing consumers. As a result transpile is currently considered deprecated and superseded by transpileModule.

varts=require("typescript");varcontent="import {f} from \"foo\"\n"+"export var x = f()";varcompilerOptions={module: ts.ModuleKind.System};varres1=ts.transpileModule(content,{compilerOptions: compilerOptions,moduleName: "myModule2"});console.log(res1.outputText);console.log("============")varres2=ts.transpile(content,compilerOptions,/*fileName*/undefined,/*diagnostics*/undefined,/*moduleName*/"myModule1");console.log(res2);

Usually TypeScript compiler uses file extension to determine if file should be parsed as '.tsx' or '.ts'. The same rule is applied during single file transpilation if the file name is provided. If the file name is omitted, then compiler will check if the jsx options is specified. If it is set and is not JsxEmit.None, then source text will be interpreted as '.tsx'.

Customizing module resolution

You can override the standard way the compiler uses to resolve modules by implementing optional method: CompilerHost.resolveModuleNames:

CompilerHost.resolveModuleNames(moduleNames: string[], containingFile: string): string[].

The method is given a list of module names in a file, and is expected to return an array of size moduleNames.length, each element of the array stores either:

  • an instance of ResolvedModule with non-empty property resolvedFileName - resolution for corresponding name from moduleNames array or
  • undefined if module name cannot be resolved.

You can invoke the standard module resolution process via calling resolveModuleName:

resolveModuleName(moduleName: string, containingFile: string, options: CompilerOptions, moduleResolutionHost: ModuleResolutionHost): ResolvedModuleNameWithFallbackLocations.

This function returns an object that stores result of module resolution (value of resolvedModule property) as well as list of file names that were considered candidates before making current decision.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*aspathfrom"path";functioncreateCompilerHost(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(),getCanonicalFileName: fileName=>ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),getNewLine: ()=>ts.sys.newLine,useCaseSensitiveFileNames: ()=>ts.sys.useCaseSensitiveFileNames,
fileExists,
readFile,
resolveModuleNames
}functionfileExists(fileName: string): boolean{returnts.sys.fileExists(fileName);}functionreadFile(fileName: string): string{returnts.sys.readFile(fileName);}functiongetSourceFile(fileName: string,languageVersion: ts.ScriptTarget,onError?: (message: string)=>void){constsourceText=ts.sys.readFile(fileName);returnsourceText!==undefined ? ts.createSourceFile(fileName,sourceText,languageVersion) : undefined;}functionresolveModuleNames(moduleNames: string[],containingFile: string): ts.ResolvedModule[]{returnmoduleNames.map(moduleName=>{// try to use standard resolutionletresult=ts.resolveModuleName(moduleName,containingFile,options,{fileExists, readFile});if(result.resolvedModule){returnresult.resolvedModule;}// check fallback locations, for simplicity assume that module at location should be represented by '.d.ts' filefor(constlocationofmoduleSearchLocations){constmodulePath=path.join(location,moduleName+".d.ts");if(fileExists(modulePath)){return{resolvedFileName: modulePath}}}returnundefined;});}}functioncompile(sourceFiles: string[],moduleSearchLocations: string[]): void{constoptions: ts.CompilerOptions={module: ts.ModuleKind.AMD,target: ts.ScriptTarget.ES5};consthost=createCompilerHost(options,moduleSearchLocations);constprogram=ts.createProgram(sourceFiles,options,host);/// do something with program...}

Using the Type Checker

In this example we will walk the AST and use the checker to serialize class information. We'll use the type checker to get symbol and type information, while grabbing JSDoc comments for exported classes, their constructors, and respective constructor parameters.

/// <reference path="typings/node/node.d.ts" />import*astsfrom"typescript";import*asfsfrom"fs";interfaceDocEntry{name?: string,fileName?: string,documentation?: string,type?: string,constructors?: DocEntry[],parameters?: DocEntry[],returnType?: string};/** Generate documention for all classes in a set of .ts files */functiongenerateDocumentation(fileNames: string[],options: ts.CompilerOptions): void{// Build a program using the set of root file names in fileNamesletprogram=ts.createProgram(fileNames,options);// Get the checker, we will use it to find more about classesletchecker=program.getTypeChecker();letoutput: DocEntry[]=[];// Visit every sourceFile in the program for(constsourceFileofprogram.getSourceFiles()){// Walk the tree to search for classests.forEachChild(sourceFile,visit);}// print out the docfs.writeFileSync("classes.json",JSON.stringify(output,undefined,4));return;/** visit nodes finding exported classes */functionvisit(node: ts.Node){// Only consider exported nodesif(!isNodeExported(node)){return;}if(node.kind===ts.SyntaxKind.ClassDeclaration){// This is a top level class, get its symbolletsymbol=checker.getSymbolAtLocation((<ts.ClassDeclaration>node).name);output.push(serializeClass(symbol));// No need to walk any further, class expressions/inner declarations// cannot be exported}elseif(node.kind===ts.SyntaxKind.ModuleDeclaration){// This is a namespace, visit its childrents.forEachChild(node,visit);}}/** Serialize a symbol into a json object */functionserializeSymbol(symbol: ts.Symbol): DocEntry{return{name: symbol.getName(),documentation: ts.displayPartsToString(symbol.getDocumentationComment()),type: checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration))};}/** Serialize a class symbol infomration */functionserializeClass(symbol: ts.Symbol){letdetails=serializeSymbol(symbol);// Get the construct signaturesletconstructorType=checker.getTypeOfSymbolAtLocation(symbol,symbol.valueDeclaration);details.constructors=constructorType.getConstructSignatures().map(serializeSignature);returndetails;}/** Serialize a signature (call or construct) */functionserializeSignature(signature: ts.Signature){return{parameters: signature.parameters.map(serializeSymbol),returnType: checker.typeToString(signature.getReturnType()),documentation: ts.displayPartsToString(signature.getDocumentationComment())};}/** True if this is visible outside this file, false otherwise */functionisNodeExported(node: ts.Node): boolean{return(node.flags&ts.NodeFlags.Export)!==0||(node.parent&&node.parent.kind===ts.SyntaxKind.SourceFile);}}generateDocumentation(process.argv.slice(2),{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

to try this:

tsc docGenerator.ts --m commonjs node docGenerator.js test.ts

Passing an input like:

/**  * Documentation for C  */classC{/**  * constructor documentation * @param a my parameter documentation * @param b another parameter documentation */constructor(a: string,b: C){}}

We should get output like:

[
{
"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"
}
]
}
]

Clone this wiki locally