Read this blog post 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:
Note: This blog post does not explain why TypeScript is useful. If you want to know more about that, you can read my TypeScript sales pitch.
Even though the following description of TypeScript is not 100% accurate (there are a few exceptions), I find it helpful for figuring out how it works: TypeScript is JavaScript plus type syntax.
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;
}
The type syntax is only used for type checking (during editing and compiling) and gives us many consistency checks and better auto-completion.
Consider the following TypeScript project:
ts-app/
tsconfig.json
src/
main.ts
util.ts
util_test.ts
test/
integration_test.ts
tsconfig.json
is a configuration file that tells TypeScript how to type-check and compile our code.Let’s explore the different ways in which we can run this code.
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
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
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
determines 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
By default, TypeScript does not change the specifiers of imported modules. That means that local imports of transpiled code must look like this:
// main.ts
import {helperFunc} from './util.js';
However, we can also tell TypeScript to rewrite the filename extension .ts
to .js
(more information). Then the following import works for both directly running our code and transpiling it:
// main.ts
import {helperFunc} from './util.ts';
The npm registry is still the most popular way of publishing packages. Even though Node.js supports app packages being written in TypeScript, library packages must be deployed as JavaScript code – so that they can be consumed by either JavaScript or TypeScript. Therefore, a single library file lib.ts
is often deployed as five files (four of which are compiled by TypeScript from lib.ts
):
lib.js
: the JavaScript part of lib.ts
lib.d.ts
: the type part of lib.ts
lib.ts
.
lib.js.map
: source map for lib.js
lib.d.ts.map
: source map for lib.d.ts
lib.ts
: the target of the previous two source maps(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/
src/
lib.js
lib.js.map
lib.d.ts
lib.d.ts.map
package.json
is npm’s description of our library package. Some of its data, such as the so-called package exports, are also used by TypeScript – e.g. to look up type information when someone imports from our package.dist/
was generated by TypeScript. It is usually not added to version control systems because it can easily be regenerated.tsconfig.json
is not uploaded to the npm registry..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:
.d.ts
files.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:
lib.js
has all comments so that the code is easier to read.lib.d.ts
only has JSDoc comments (/** */
) because they are used by many IDEs to display inline documentation.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.:
lib.js.map
: maps lib.js
locations to lib.ts
locations and gives us debugging and stack traces for the latter when we run the former.lib.d.ts.map
: maps lib.d.ts
lines to lib.ts
lines. It enables “go to definition” for imports from lib.ts
to take us to that file.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.
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.
tsc
Let’s recap all the tasks performed by tsc
(we’ll ignore source maps in this section):
#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.
Type stripping is the simplest way of compiling TypeScript to JavaScript:
The second point means that several TypeScript features are not supported – e.g.:
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.
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.
“Isolated declaration” is a style of writing TypeScript where the types for a declaration file (.d.ts
) are easy to extract. That mainly means providing return types for functions that are exported. In principle, TypeScript can figure those out for us, but simple declaration file generators can’t. This constraint does not exist for unexported functions because those don’t show up in declaration files.
First version of a TypeScript file strings.ts
:
// Not OK: exported function without return type
export function upperCase(str: string) {
return str.toUpperCase();
}
// Not exported, no return type needed
function internalHelper() {}
strings.ts
in isolated declaration style:
// OK: exported function has return type
export function upperCase(str: string): string {
return str.toUpperCase();
}
// Not exported, no return type needed
function internalHelper() {}
This is the generated declaration file strings.d.ts
(note that internalHelper
is not in it):
export declare function upperCase(str: string): string;
The JavaScript registry JSR is an alternative to npm and the npm registry for publishing packages. It works as follows:
.ts
files..js
files and .d.ts
files and installs those, along with the .ts
files. To make automatic generation possible, the TypeScript code must follow a set of rules called “no slow types” – which is similar to isolated declarations.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.
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.
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:
Any file that is currently open is automatically type-checked within Visual Studio Code. It order to provide that functionality, it comes with its own installation of TypeScript.
If we want to type-check all of a code base, we must invoke the TypeScript compiler tsc
. We can do that via Visual Studio Code’s tasks – a built-in way of invoking external tools (for type checking, compiling, bundling, etc.). The official documentation has more information on tasks.
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:
.d.ts
files from .js
files with JSDoc comments. That is an extra build step, though. How to do that is explained in the TypeScript Handbook.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:
tsconfig.json
”