TypeScript is an extension of JavaScript that is a superset of JavaScript and adds a static type checking system to JavaScript. TypeScript allows us to discover type-definition inconsistencies during development, eliminating hidden risks, and greatly improving the readability and maintainability of code. Since you’re already familiar with how to use TypeScript in your projects, this article will briefly explore how TypeScript works and what tools are available to help it achieve this goal.

How TypeScript works

Here’s how peScript roughly works:

  1. TypeScript source code is scanned by a scanner as a set of tokens;
  2. The parser parses the token and gets an AST syntax tree.
  3. The binder traverses the AST syntax tree to generate a series of symbols and concatenates these symbols to the corresponding nodes;
  4. The inspector scans the AST again, checking for type and collecting errors;
  5. Emitters generate JavaScript code based on the AST.

As you can see, the AST is the heart of the whole type validation. As in the following code

var a = 1;
function func(p: number): number {
    return p * p;
}
a = 's'
export {
    func
}
Copy the code

The structure of AST generation isA Node in the AST is called a Node, which records the type and location of the Node in the source code. Different types of Nodes record different information. For example, for a Node of FunctionDeclaration type, information such as name, parameters, and body will be recorded. For a Node of VariableDeclaration type, Information such as name and initializer will be recorded. A SourceFile is also a Node — SourceFile, which is the root Node of the AST.

There are a lot of theories about how to generate the AST from source code, as well as the final code from the AST, and I won’t go over them in this article. This section focuses on the role of the binder and how the inspector checks types.

In short, the ultimate goal of the binder is to assist the inspector in type checking by traversing the AST, generating a Symbol for each Node, and associating the associated parts of the source code (at the AST Node level). This sentence may not be intuitive, but let’s illustrate it.

Symbol, the basic building block of the semantic system, has two basic properties: Members and exports. Members records class, interface, or literal instance members, and exports records objects exported by modules. Symbols is the symbol of an object, or the external identity of an object. For example, when we use a class instance object, we only care what variables/methods the object provides. When we use a module, we only care about what objects the module exports. We can obtain this information by reading the Symbol.

Then look at how the binder correlates the related parts of the source code (at the AST node level). This requires two more properties: the Node locals property and the Symbol declarations property. For container-type nodes, there is a locals property, which records variables/classes/types/functions declared in the Node. For the func function in the above code, there is an attribute P in locals on the FunctionDeclaration node. The SourceFile node contains both a and func attributes.

The Symbol’s declarations property records the declaration node of the variable that corresponds to the Symbol. The Symbol declarations in line 1 and 7 are the same as those in the Symbol declarations. The Symbol declarations in line 1 and Symbol declarations are the same as those in line 7. The corresponding VariableDeclaration node.

The Symbol’s declarations property is an array. Normally, there is only one object in the array. An example of this violation is interface declarations, which can be merged in TypeScript. As in the following example

interface T {
    a: string
}
interface T {
    b: number
}
Copy the code

The generated AST tree isContains two InterfaceDeclaration nodes, which is as expected. But for the two InterfaceDeclaration nodes, the associated Symbol is

The members of both declarations are merged, and the declarations also contain two records.

Now that you understand what the binder does, it’s pretty clear how the inspector works. The Node and Symbol are associated. The Node contains the type information associated with the Node, and the Symbol contains the exposed variables of the Node and the corresponding declaration Node of the Symbol. For assignment operations, check that the value assigned to the Node matches the Node type. For the import operation, check that Symbol exports the variable. For object invocation operations, find the Symbol of the calling method from the Symbol’s Members property, find the corresponding declaration node based on that Symbol, and loop through. The implementation will not be studied here.

The inspection results are logged to the Diagnostics property of the SourceFile node.

TypeScript vs VSCode

When we create a new TypeScript file in VSCode and type TS code, we can see that VSCode automatically highlights the code and even red flags the inconsistent types to indicate type errors.This is because VSCode has built-in support for the TypeScript language, and type checking is done primarily through TypeScript extensions. Language Service Protocal is behind the plug-in.

Language Service Protocal

LSP is a protocol proposed by Microsoft to solve the problem of plug-in reuse between different editors. LSP isolates the language plug-in from the editor. Instead of communicating with the editor directly, the plug-in forwards packets through LSP. In this way, plug-ins with the same function can be written at one time and run in multiple places in lSP-compliant compilers.As shown in the figure, the plug-in that follows THE LSP protocol has two parts

  1. LSP client used to interact with VSCode environment. It is usually written in JS/TS and has access to the VSCode API, so it can listen for events passed by VSCode or send notifications to VSCode.
  2. Language server. It is the core implementation of language features, used for textual lexical analysis, grammar analysis, semantic diagnosis and so on. It runs in a separate process.

The TypeScript plug-in

VSCode has built-in support for TypeScript, which means that VSCode has built-in TypeScript plug-ins.

You can see this by searching for typescript in Preference and finding typescript under Extensions. By changing the configuration, you can control the behavior of your plug-in.

TypeScript plug-ins also follow the LSP protocol. The LSP protocol mentioned above is intended to allow the plug-in to be written in multiple places at once, which is actually more targeted at the language server part. This is because the program analysis functions are implemented by the language server, which is the most workload. This section starts with the language server.

tsserver

The language server for a TypeScript plug-in is simply a tsServer.js file that runs in a separate process. We can find tsserver in typescript source under the SRC file folder, the folder after compilation, is our project in node_modules/typescript/lib/tsserver js file. Tsserver receives various messages sent by the plug-in client, and sends the files to typescript-Core for analysis and processing. After the processing results are sent back to the client, the plug-in client gives VSCode to display/perform actions.

Since TypeScript plug-ins don’t need to compile TS files into JS files, typescript-core only runs as far as the inspector.

private semanticCheck(file: NormalizedPath, project: Project) {
    / / simplify
    const diags = project.getLanguageService().getSemanticDiagnostics(file).filter(d= >!!!!! d.file);this.sendDiagnosticsEvent(file, project, diags, "semanticDiag");
}
Copy the code

Basically, the name tells you what this function does.

The TypeScript plug-in creates tsServer statements as

this._factory.fork(version.tsServerPath, args, kind, configuration, this._versionManager)
Copy the code

It is obvious that a process has been forked. The notable argument in fork is version.tsServerPath, which is the path to the tsServer.js file. When we hover over the version of TypeScript in the lower-right corner of the status bar, we will be prompted for the tsServer.js file used by the current plug-in.VSCode builds the latest stable version of typescript and uses this version of the tsserver.js file to create the language server. This corresponds to the version of typescript that you rely on in the workspace version — package.json. Clicking on the TypeScript version in the lower right corner of the status bar prompts you to switch to the tsServer version. If the TSServer version changes, the language server process is recreated.

The LSP client

The LSP client provides the following functions:

  1. Create a language server;
  2. Act as a bridge between VSCode and the language server.

Creating a language server is basically a process that forks, communicates with the language server through interprocess communication, and communicates with VSCode by calling the VSCode namespace API.

Features such as highlighting and pop-ups are required by many languages, so VSCode presets the UI and actions, and the LSP client only needs to provide the corresponding data. As for the diagnosis of grammar VSCode provides createDiagnosticCollection method, need grammar diagnosis function plugin only need to call this method to create a DiagnosticCollection object, then the diagnosis by file can be added to this object. When the TypeScript plug-in creates an LSP client, it associates the client with a DiagnosticsManager object.

class DiagnosticsManager {

    constructor(owner: string, onCaseInsenitiveFileSystem: boolean) {
        super(a);// Three objects were created, _diagnostics and _pendingUpdate, to be used primarily as caching for performance optimization
        / / _currentDiagnostics is the core diagnosis object, call the createDiagnosticCollection
        this._diagnostics = new ResourceMap<FileDiagnostics>(undefined, { onCaseInsenitiveFileSystem });
        this._pendingUpdates = new ResourceMap<any>(undefined, { onCaseInsenitiveFileSystem });
        this._currentDiagnostics = this._register(vscode.languages.createDiagnosticCollection(owner));
    }

    public updateDiagnostics(
        file: vscode.Uri,
        language: DiagnosticLanguage,
        kind: DiagnosticKind,
        diagnostics: ReadonlyArray<vscode.Diagnostic>
    ): void {
        // there is a simplification of creating a fileDiagnostics object for each file and logging the diagnostics to the fileDiagnostics object
        // An update event is triggered after file and File Diagnostics are associated to the _Diagnostics object
        const fileDiagnostics = new FileDiagnostics(file, language);
        fileDiagnostics.updateDiagnostics(language, kind, diagnostics);
        this._diagnostics.set(file, fileDiagnostics);
        this.scheduleDiagnosticsUpdate(file);
    }

    private scheduleDiagnosticsUpdate(file: vscode.Uri) {
        if (!this._pendingUpdates.has(file)) {
            // Delay the update
            this._pendingUpdates.set(file, setTimeout(() => this.updateCurrentDiagnostics(file), this._updateDelay)); }}private updateCurrentDiagnostics(file: vscode.Uri): void {
        if (this._pendingUpdates.has(file)) {
            clearTimeout(this._pendingUpdates.get(file));
            this._pendingUpdates.delete(file);
        }
        // Actually triggers the updated code, fetching the file's associated diagnostics from _diagnostics and setting it into the _currentDiagnostics object
        // Trigger the update
        const fileDiagnostics = this._diagnostics.get(file);
        this._currentDiagnostics.set(file, fileDiagnostics ? fileDiagnostics.getDiagnostics(this._settings) : []); }}Copy the code

After receiving the diagnosis result from the language server, the LSP client invokes the updateDiagnostics method of DiagnosticsManager, and the diagnosis result is displayed on VSCode.

TypeScript and Babel

During development, the error notification function is provided by VSCode. But what processes TypeScript when our code has to be compiled to run in the browser? The answer is Babel. Babel was originally designed to convert ECMAScript 2015+ code into backwardly compatible code, mainly through syntactic conversions and polyfills. As long as Babel recognizes TypeScript syntax, it can transform TypeScript syntax. As a result, Babel and the TypeScript team collaborated for a year to release the @babel/ Preset – TypeScript plugin. Using this plug-in, you can convert TypeScript to JavaScript.

Babel can be used in two common scenarios, either by calling the Babel command directly from the CLI or by combining Babel with a packaging tool such as Webpack. Since Babel doesn’t pack itself, calling Babel directly from the command line is of little use. This section focuses on how Babel works with typescript in Webpack. Using the @babel/ Preset -typescript plug-in in Webpack is simple and only requires two steps. The first step is to configure Babel to load the @babel/preset-typescript plug-in

{
    "presets": ["@babel/preset-typescript"]}Copy the code

Then configure WebPack so that Babel can handle TS files

{
    "rules"[{"test": /.ts$/."use": "label-loader"}}]Copy the code

This way, when Webpack encounters a. Ts file, it calls label-Loader to process it. Label-loader converts this file into a standard JavaScript file and returns the processing results to Webpack, which continues the process. How does label-loader convert TypeScript files to standard JavaScript files? The answer is to simply delete the type annotations. Taking a look at the workflow of Babel, there are three main steps: parsing, transformation, and generation.

  1. Parsing: Process the source code as an AST. Corresponding to the Babel – parse
  2. Transformation: Traverses the AST, adding, updating, and removing nodes during this process. Corresponding to the Babel – tranverse.
  3. Generation: Converts the transformed AST into string code and creates a source map. Corresponding to the Babel – the generator.

How do these three steps of Babel work now that @babel/preset-typescript is added

  1. Parsing: Call the typescript plug-in of Babel-Parser to process the source code into an AST.

  2. Conversion: The babel-plugin-transform-typescript plugin is called during babel-tranverse, and the type annotation node is removed when it is encountered.

  3. Generation: When a type annotation type node is encountered, the corresponding output method is called. Everything else is as usual.

With Babel, you can not only work with typescript, but also enjoy the polyfill functionality that existed before Babel. And because Babel only removes type annotation nodes, it’s pretty fast. So what’s the point of writing TypeScript now that Babel has removed type annotations? I think there are mainly the following considerations:

  1. In terms of performance, type annotations are the fastest to remove. Collecting the type and verifying that the type is correct is a time-consuming operation.
  2. Babel’s own limitations. As discussed in the first section of this article, all files in the project need to be parsed to collect type information before type validation. Babel is just a single-file processing tool. Webpack is also called file by file when it calls loader to process files. So Babel can’t verify the type. And Babel does not output errors in any of its three working steps.
  3. There’s no need. Type validation errors can be submitted to the editor.

Of course, due to the single-file nature of Babel, @babel/preset-typescript is not well supported for typescript language features such as const enums that require the full type system information to run correctly. See the documentation for full information.

Four, TSC

VSCode only prompts type errors. Babel does not verify type at all. What if we want to ensure that the code submitted to the repository is typed correctly? You can use the TSC command.

tsc --noEmit --skipLibCheck
Copy the code

Simply run this command in your project to type check your project code. If working with Husky, execute this command to check the type before gitCommit. If type validation does not pass, git commit will not be executed, and the entire development experience will be perfect.

The TSC command corresponds to the TypeScript version installed in node_modules, which may be different from the TSServer version used by VSCode’s TypeScript plug-in. This is fine in most cases; VSCode’s built-in version of TypeScript is generally higher than the version of TypeScript that projects rely on, and TypeScript is backward compatible. If VSCode type check works but TSC command check fails, or vice versa, check the version.

Five, the summary

This article explores how TypeScript works and the tools that make it useful in project development. I hope I can give you some inspiration.

The appendix

  • The TypeScript AST Viewer. Make sure the Binding Option in Option is turned on.