HomepageExploring TypeScript (TS 5.8 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

6 How TypeScript is used: workflows, tools, etc.

Read this chapter if you are a JavaScript programmer and want to get a rough idea of what using TypeScript is like (think first step before learning more details). You’ll get answers to the following questions:

This chapter focuses on how TypeScript works. If you want to know more about why it is useful, see “Sales pitch for TypeScript” (§2).

6.1 TypeScript is JavaScript plus type syntax

Let’s start with a rough first description of what TypeScript is. That description is not completely accurate (there are exceptions and details that I’m omitting), but it should give you a solid first idea:

TypeScript is JavaScript plus type syntax.

What are the purposes of these two parts?

Consider the following TypeScript code:

function add(x: number, y: number): number {
  return x + y;
}

If we want to run this code, we have to remove the type syntax and get JavaScript that is executed by a JavaScript engine:

function add(x, y) {
  return x + y;
}

6.2 Ways of running TypeScript code

Consider the following TypeScript project:

ts-app/
  tsconfig.json
  src/
    main.ts
    util.ts
    util_test.ts
  test/
    integration_test.ts

Let’s explore the different ways in which we can run this code.

6.2.1 Running TypeScript directly

Most server-side runtimes now can run TypeScript code directly – e.g., Node.js, Deno and Bun. In other words, the following works in Node.js 23.6.0+:

cd ts-app/
node src/main.ts

6.2.2 Bundling TypeScript

When developing a web app, bundling is a common practice – even for pure JavaScript projects: All the JavaScript code (app code and library code) is combined into a single JavaScript file (sometimes more, but never more than a few) – which is typically loaded from an HTML file. That has several benefits:

Most bundlers support TypeScript – either directly or via plugins. That means, we run our TypeScript code via the JavaScript file bundle.js that was produced by a bundler:

ts-app/
  tsconfig.json
  src/
    main.ts
    util.ts
    util_test.ts
  test/
    integration_test.ts
  dist/
    bundle.js

6.2.3 Transpiling TypeScript to JavaScript

Another option is to compile out TypeScript app to JavaScript via the TypeScript compiler tsc and run the resulting code. Before server-side JavaScript runtimes had built-in support for TypeScript, that was the only way we could run TypeScript there.

Compiling source code to source code is also called transpiling. tsconfig.json specifies where the transpilation output is written. Let’s assume we write it to the directory dist/:

ts-app/
  tsconfig.json
  src/
    main.ts
    util.ts
    util_test.ts
  test/
    integration_test.ts
  dist/
    src/
      main.js
      util.js
      util_test.js
    test/
      integration_test.js

6.2.4 The filename extensions of locally imported TypeScript modules

When it comes to filename extensions of locally imported TypeScript modules, we must distinguish between code that is transpiled and code that is run directly.

By default, TypeScript does not change the specifiers of imported modules. Therefore, code that is transpiled must look like this (we import util.js, from JavaScript code):

// main.ts
import {helperFunc} from './util.js';

However, such code does not work if we run it directly. There, we must write (we import util.ts from TypeScript code):

// main.ts
import {helperFunc} from './util.ts';

We can also tell TypeScript to change the filename extensions of local imports from .ts to .js (more information). Then the previous code can also be transpiled.

6.3 Publishing a library package to the npm registry

The npm registry is still the most popular means of publishing packages. Even though Node.js runs TypeScript code, packages must be deployed as JavaScript code. That enables JavaScript code to use library packages written in TypeScript. However, we additionally want to support TypeScript features. Therefore, a single library file lib.ts is often deployed as five files (four of which are compiled by TypeScript from lib.ts):

(More on what all of that means in a second.)

As an example, consider the following library package:

ts-lib/
  package.json
  tsconfig.json
  src/
    lib.ts
  dist/
    lib.js
    lib.js.map
    lib.d.ts
    lib.d.ts.map

6.3.1 Essential: .js and .d.ts

It’s interesting to see the combined JavaScript plus types in .lib.ts be split into lib.js with only JavaScript and lib.d.ts with only types. Why do that? It enables library packages to be used by either JavaScript code or TypeScript code:

Actually, behind the scenes, many editors (e.g. Visual Studio Code) use a kind of lightweight TypeScript mode when editing JavaScript code so that we also get simple type checking and code completion there.

This is the TypeScript input lib.ts

/** Add two numbers. */
export function add(x: number, y: number): number {
  return x + y; // numeric addition
}

It is split into lib.js on one hand:

/** Add two numbers. */
export function add(x, y) {
    return x + y; // numeric addition
}
//# sourceMappingURL=lib.js.map

And lib.d.ts on the other hand:

/** Add two numbers. */
export declare function add(x: number, y: number): number;
//# sourceMappingURL=lib.d.ts.map

Notes:

6.3.2 Optional: source maps

If we compile a file I to a file O then a source map for O maps source code locations in O to source code locations in I. That means we can work with O but display information from I – e.g.:

All source-map-related functionality except stack traces require access to the original TypeScript source code. That’s why it makes sense to include lib.ts if there are source maps.

This is what lib.js.map looks like:

{
  "version": 3,
  "file": "lib.js",
  "sourceRoot": "",
  "sources": [
    "../../src/lib.ts"
  ],
  "names": [],
  "mappings": "AAAA,uBAAuB;AACvB,MAAM,UAAU,···"
}

This is what lib.d.ts.map looks like:

{
  "version": 3,
  "file": "lib.d.ts",
  "sourceRoot": "",
  "sources": [
    "../../src/lib.ts"
  ],
  "names": [],
  "mappings": "AAAA,uBAAuB;AACvB,wBAAgB,GAAG,···"
}

In both cases, the actual content of "mappings" was abbreviated. And in the actual output of tsc, the JSON is always squeezed into a single line.

6.4 DefinitelyTyped: a repository with types for type-less npm packages

These days, many npm packages come with TypeScript types. However, not all of them do. In that case, DefinitelyTyped may help: If it supports a type-less package pkg then we can additionally install a package @types/pkg with types for pkg.

One important DefinitelyTyped package for Node.js is @types/node with types for all of its APIs. If you develop TypeScript on Node.js, you will usually have this package as a development dependency.

6.5 Compiling TypeScript with tools other than tsc

Let’s recap all the tasks performed by tsc (we’ll ignore source maps in this section):

  1. It compiles TypeScript files to JavaScript files.
  2. It compiles TypeScript files to type declaration files.
  3. It type-checks TypeScript files.

##3 is so complex that only tsc can do it. However, for both #1 and #2, there are slightly simpler subsets of TypeScript where compilation does not involve much more than syntactic processing. That means that we can use external, faster tools for #1 and #2.

There are even tsconfig.json settings to warn us if we don’t stay within those subsets of TypeScript (more information). Doing that is not much of a sacrifice in practice.

6.5.1 Type stripping

Type stripping is a simple and fast way of compiling TypeScript to JavaScript. It’s what Node.js uses when it runs TypeScript. Type stripping is fast because it only supports a subset of TypeScript where two things are possible:

  1. Type syntax can be detected and removed by only parsing the syntax – without performing additional semantic analyses.

  2. No non-type language features are transpiled. In other words: Removing the type syntax is enough to produce JavaScript.

#2 means that there are several TypeScript features that we can’t use – e.g., enums and JSX (HTML-like syntax inside TypeScript, as used, e.g., by React).

One considerable benefit of type stripping is that it does not need any configuration (via tsconfig.json or other means) because it’s so simple. That makes platforms that use it more stable w.r.t. changes made to TypeScript.

6.5.1.1 Type stripping technique: replacing types with spaces

One clever technique for type stripping was pioneered by the ts-blank-space tool (by Ashley Claymore for Bloomberg): Instead of simply removing the type syntax, it replaces it with spaces. That means that source code positions in the output don’t change. Therefore, any positions that show up (e.g.) in stack traces still work for the input and there is less of a need for source maps: You still need them for debugging and going to definitions but JavaScript generated by type stripping is relatively close to the original TypeScript and you are often OK even then.

For example - input (TypeScript):

function add(x: number, y: number): number {
  return x + y;
}

Output (JavaScript):

function add(x        , y        )         {
  return x + y;
}

If you want to explore further, you can check out the ts-blank-space playground.

6.5.2 Isolated declarations

“Isolated declaration” is a style of writing TypeScript that makes it easier for external tools to generate declaration files. It mainly means we have to add type annotations in locations where the TypeScript compiler tsc does not need them – thanks to its ability to automatically derive types (so-called type inference). However, external tools are simpler and faster if they don’t need that ability. Additionally, they don’t have to visit and analyze external files if the type information is provided locally.

The following example shows how the isolated declaration style changes code:

// OK: return type stated explicitly
export function f1(): string {
  return 123..toString();
}
// Error: return type requires inference
export function f2() {
  return 123..toString();
}
// OK: return type trivial to determine
export function f3() {
  return 123;
}

Note that isolated declarations only affect constructs that are exported. Module-internal code does not show up in declaration files.

6.6 JSR – the JavaScript registry

The JavaScript registry JSR is an alternative to npm and the npm registry for publishing packages. It works as follows:

In contrast, with the npm registry, your TypeScript library package is only usable on Node.js if you upload .js files and .d.ts files.

JSR also provides several features that npm doesn’t such as automatic generation of documentation. See “Why JSR?” in the official documentation for more information.

6.6.1 Who owns JSR?

Quoting the official documentation page “Governance”:

JSR is not owned by any one person or organization. It is a community-driven project that is open to all, built for the entire JavaScript ecosystem.

JSR is currently operated by the Deno company. We are currently working on establishing a governance board to oversee the project, which will then work on moving the project to a foundation.

6.7 Editing TypeScript

Two popular IDEs for JavaScript are:

The observations in this section are about Visual Studio Code, but may apply to other IDEs, too.

With Visual Studio Code, we get two different ways of type checking:

6.8 Type-checking JavaScript files

Optionally, TypeScript can also type-check JavaScript files. Obviously that will only give us limited results. However, to help TypeScript, we can add type information via JSDoc comments – e.g.:

/**
 * @param {number} x - The first operand
 * @param {number} y - The second operand
 * @returns {number} The sum of both operands
 */
function add(x, y) {
  return x + y;
}

If we do that, we are still writing TypeScript, just with a different syntax.

Benefits of this approach:

Downside of this approach:

To explain the downside – consider how we define an interface in TypeScript:

interface Point {
  x: number;
  y: number;
  /** optional property */
  z?: number;
}

Doing that via a JSDoc comment looks like this:

/**
 * @typedef Point
 * @prop {number} x
 * @prop {number} y
 * @prop {number} [z] optional property
 */

More information in the TypeScript Handbook: