How does TypeScript work? The bird’s eye view

[2020-04-18] dev, javascript, typescript
(Ad, please don’t block)

This blog post gives the bird’s eye view of how TypeScript works: What is the structure of a typical TypeScript project? What is compiled and how? How can we use IDEs to write TypeScript?

This post is meant to be read before learning how to write TypeScript code (material for doing that is listed at the end).

The structure of TypeScript projects  

This is one possible file structure for TypeScript projects:

typescript-project/
  dist/
  ts/
    src/
      main.ts
      util.ts
    test/
      util_test.ts
  tsconfig.json

Explanations:

  • Directory ts/ contains the TypeScript files:
    • Subdirectory ts/src/ contains the actual code.
    • Subdirectory ts/test/ contains tests for the code.
  • Directory dist/ is where the output of the compiler is stored.
  • The TypeScript compiler compiles a TypeScript file such as ts/src/main.ts to a JavaScript file dist/src/main.js (and possibly other files).
  • tsconfig.json is used to configure the TypeScript compiler.

tsconfig.json  

The contents of tsconfig.json look as follows:

{
  "compilerOptions": {
    "rootDir": "ts",
    "outDir": "dist",
    "module": "commonjs",
    ···
  }
}

We have specified that:

  • The root directory of the TypeScript code is ts/.
  • The directory where the TypeScript compiler saves its output is dist/.
  • The module format of the output files is CommonJS.

Programming TypeScript via an integrated development environment (IDE)  

Visual Studio Code is one of the most popular IDEs for writing TypeScript code. In order to use it well, we need to understand that TypeScript source code is processed in two independent ways:

  • Checking open editors for errors: This is done via a so-called language server. They are an editor-independent way of providing editors with language-related services (detecting errors, refactorings, auto-completions, etc.). Editors (such as IDEs) communicate with language servers via a special protocol (JSON-RPC, i.e. JSON-based remote procedure calls). That enables one to write such servers in almost any programming language.

    • Important fact to remember: The language server only lists errors for currently open editors and it doesn’t compile TypeScript, it only analyzes it statically.
  • Building (compiling TypeScript files to JavaScript files): Here, we have two choices.

    • We can run a build tool via a command line. For example, the TypeScript compiler tsc has a --watch mode that watches input files and compiles them to output files whenever they change. As a consequence, whenever we save a TypeScript file in the IDE, we immediately get the corresponding output file(s).
    • We can run tsc from within Visual Studio Code. In order to do so, it must be installed either inside project that we are currently working on or globally (via the Node.js package manager npm).

    With building, we get a complete list of errors. For more information on compiling TypeScript in Visual Studio Code, see the official documentation for that IDE.

Other files produced by the TypeScript compiler  

Given a TypeScript file main.ts, the TypeScript compiler can produce several kinds of artifacts. The most common ones are:

  • JavaScript file: main.js
  • Declaration file: main.d.ts (contains type information; think .ts file minus the JavaScript code)
  • Source map file: main.js.map

TypeScript is often not delivered via .ts files, but via .js files and .d.ts files:

  • The JavaScript code contains the actual functionality and can be consumed via plain JavaScript.
  • The declaration files help programming editors with auto-completion and similar services. This information enables plain JavaScript to be used via TypeScript. However, we even profit from it if we work with plain JavaScript because it gives us better auto-completion and more.

A source map specifies for each part of the output code in main.js, which part of the input code in main.ts produced it. Among other things, this information enables runtime environments to execute JavaScript code, while showing the line numbers of the TypeScript code in error messages.

In order to use npm packages from TypeScript, we need type information  

The npm registry is a huge repository of JavaScript code. If we want to use a JavaScript package from TypeScript, we need type information for it:

  • The package itself may include .d.ts files or even the complete TypeScript code.
  • If it doesn’t, we may still be able to use it: DefinitelyTyped is a repository of declaration files that people have written for plain JavaScript packages.

The declaration files of DefinitelyTyped reside in the @types namespace. Therefore, if we need a declaration file for a package such as lodash, we have to install the package @types/lodash.

Using the TypeScript compiler for plain JavaScript files  

The TypeScript compiler can also process plain JavaScript files:

  • With the option --allowJs, the TypeScript compiler copies JavaScript files in the input directory over to the output directory. Benefit: When migrating from JavaScript to TypeScript we can start with a mix of JavaScript and TypeScript files and slowly convert more JavaScript files to TypeScript.

  • With the option --checkJs, the compiler additionally type-checks JavaScript files (--allowJs must be on for this option to work). It does so as well as it can, given the limited information that is available.

    • If a JavaScript file contains the comment // @ts-nocheck, it will not be type-checked.
    • Without --checkJs, the comment // @ts-check can be used to type-check individual JavaScript files.
  • The TypeScript compiler uses static type information that is specified via JSDoc comments (see below for an example). If we are thorough, we can fully statically type plain JavaScript files and even derive declaration files from them.

  • With the option --noEmit, the compiler does not produce any output, it only type-checks files.

This is an example of a JSDoc comment that provides static type information for a function add():

/**
 * @param {number} x - A number param.
 * @param {number} y - A number param.
 * @returns {number} This is the result
 */
function add(x, y) {
  return x + y;
}

More information: Type-Checking JavaScript Files in the TypeScript Handbook.