Learn / TypeScript
LESSON 32 OF 84

TypeScript Foundation

Why TypeScript exists

See a bug that JavaScript runs without complaint, install TypeScript, write a tsconfig.json, and learn the three ways to run TypeScript on Node.js 24.

  • 35 min to read and try
  • You need: The JavaScript, Node.js and npm lessons
  • You build: A ts-tasks project you can check with tsc, run with tsx or node, and compile to JavaScript
Test yourself

A bug JavaScript lets through

Here is a small piece of the Task API in plain JavaScript. Read it carefully before you run it. There is a mistake.

report.js
const task = { id: 1, title: "Buy milk", done: false };

function report(task) {
  return `#${task.id} ${task.titel} (${task.done ? "done" : "open"})`;
}

console.log(report(task));
Output of node report.js and of the browser terminal
#1 undefined (open)

The property is spelled titel. JavaScript did not complain. It read a property that does not exist, got undefined, and printed it. You saw this trap in the objects lesson. In a real API that undefined would be sent to users or saved to the database, and nobody would notice until much later.

JavaScript finds mistakes like this only while the program runs, and only on the lines that actually run. A test can catch it, as you saw in the testing lesson, but only if somebody wrote that test. In a big codebase, with many people changing code every day, that is not enough.

What TypeScript adds

TypeScript is JavaScript plus types: notes in the code that say what shape each value has. A program called the compiler, tsc, reads the notes and reports mistakes before the code ever runs. This is called type checking, and because it happens without running the code, it is also called static checking.

JavaScriptTypeScript
File namereport.jsreport.ts
TypesValues have types, but the code does not say whichYou write them, or the compiler works them out
Mistakes foundWhile the program runsBefore it runs, by tsc or your editor
What Node.js runsThe file itselfJavaScript: the same file with the types removed

Every JavaScript feature you know still works. TypeScript only adds the type notes, and removes them again before the code runs. That is why ZudoJS, like most large Node.js projects today, is written in TypeScript: the compiler checks every call between thousands of files each time you save.

Install TypeScript

Make a new project folder for the TypeScript lessons. You will use it until the end of this part of the course:

Terminal on your computer
mkdir ts-tasks
cd ts-tasks
npm init -y
npm pkg set type=module
npm install -D typescript tsx @types/node
added 7 packages, and audited 8 packages in 5s

found 0 vulnerabilities
npm warn install-scripts 1 package has install scripts not yet covered by allowScripts:
npm warn install-scripts   esbuild@0.28.2 (postinstall: node install.js)
npm warn install-scripts
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.

You installed three packages as dev dependencies (-D), which you met in the npm lesson: tools you use while writing code, not code your app needs to run.

  • typescript gives you tsc, the TypeScript compiler. It checks your types, and it can turn .ts files into .js files.
  • tsx runs a .ts file directly, like node runs a .js file.
  • @types/node describes Node.js itself to TypeScript, for example what console.log and node:fs accept.

ABOUT THAT WARNING

Recent versions of npm ask before running a package's install script. esbuild, which tsx uses, has one that only double-checks its download. tsx works without it. If you want the warning gone, run npm install-scripts approve esbuild, which records your approval in package.json. Your version numbers and timings may differ from the ones shown.

Check the tools:

Terminal on your computer
npx tsc --version
Version 7.0.2
npx tsx --version
tsx v4.23.15
node v24.19.0

npx runs a program from your project's node_modules folder, so you do not need to install anything globally. TypeScript 7 is the first version with the new native compiler, which checks a large project many times faster than before.

Tell TypeScript about your project

The compiler reads its settings from a file called tsconfig.json in the project folder. npx tsc --init writes a long one full of comments. You will write a short one instead, so you know what every line does:

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2024",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "types": ["node"],
    "verbatimModuleSyntax": true
  }
}

What each setting means:

SettingWhat it does
targetWhich JavaScript version to produce. ES2024 is modern JavaScript that Node.js 24 runs as it is, so the compiler does not rewrite your code into older syntax.
moduleWhich kind of modules to produce. NodeNext means "whatever Node.js does": ES modules here, because package.json says "type": "module".
moduleResolutionHow import paths are found. NodeNext uses Node's own rules. Modules in TypeScript explains them.
strictTurns on every safety check, for example "this value might be undefined". Always use it. It is what makes TypeScript worth having.
noEmittsc only checks; it writes no files. Something else does the running.
skipLibCheckDo not re-check the type files of installed packages. They were checked when they were published, and skipping them makes tsc faster.
typesWhich installed type packages to load. ["node"] loads @types/node, so TypeScript knows process, Buffer and node:fs.
verbatimModuleSyntaxMakes you mark imports that are only types with import type. ZudoJS code follows this rule, and Modules in TypeScript shows why.

Your first TypeScript file

Save the same code as report.ts, and add one thing: a type that describes a task, and a note on the parameter saying task is one of those. The note is called an annotation: a colon and a type after a name.

report.ts
type Task = {
  id: number;
  title: string;
  done: boolean;
};

const task: Task = { id: 1, title: "Buy milk", done: false };

function report(task: Task): string {
  return `#${task.id} ${task.titel} (${task.done ? "done" : "open"})`;
}

console.log(report(task));
What npx tsc --noEmit prints
report.ts:10:30 - error TS2551: Property 'titel' does not exist on type 'Task'. Did you mean 'title'?

10   return `#${task.id} ${task.titel} (${task.done ? "done" : "open"})`;
                                ~~~~~

  report.ts:3:3 - 'title' is declared here.
    3   title: string;
        ~~~~~


Found 1 error in report.ts:10

Run the compiler. noEmit is already in tsconfig.json, but writing --noEmit keeps the command's meaning clear:

Terminal on your computer
npx tsc --noEmit

It prints the error shown above. It names the file, line 10 and column 30, marks the exact spot with ~~~~~, and even suggests the fix: Did you mean 'title'? Every error also has a number, here TS2551. Search for it when a message is unclear. The bug was caught without running the program at all.

Fix the typo and check again. When tsc prints nothing, there are no errors. Then run the file with tsx:

report.ts
type Task = {
  id: number;
  title: string;
  done: boolean;
};

const task: Task = { id: 1, title: "Buy milk", done: false };

function report(task: Task): string {
  return `#${task.id} ${task.title} (${task.done ? "done" : "open"})`;
}

console.log(report(task));
Output of npx tsx report.ts and of the browser terminal
#1 Buy milk (open)
Terminal on your computer
npx tsc --noEmit
npx tsx report.ts
#1 Buy milk (open)

type Task = { ... } gives a name to an object shape. Interfaces, unions and literal types covers the ways to name types. The : string after the parameter list is the return type: the compiler checks that report really returns a string.

You do not have to write every type

TypeScript works out most types from the values. This is called type inference. None of these variables has an annotation, and all of them are still checked:

infer.ts
const title = "Buy milk";
let count = 0;
const tags = ["home", "shop"];

count = count + tags.length;
console.log(title.toUpperCase(), count, tags.join("+"));
Output of npx tsx infer.ts and of the browser terminal
BUY MILK 2 home+shop

The compiler knows title is a string, count a number and tags an array of strings. So it refuses a wrong value later:

infer.ts
let count = 0;
count = "none";

const tags = ["home", "shop"];
tags.push(42);
What npx tsc --noEmit prints
infer.ts:2:1 - error TS2322: Type 'string' is not assignable to type 'number'.

2 count = "none";
  ~~~~~

infer.ts:5:11 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

5 tags.push(42);
            ~~


Found 2 errors in the same file, starting at: infer.ts:2

The rule of thumb for the rest of the course: annotate function parameters, because TypeScript cannot guess what callers will pass, and let it infer the rest. In VS Code, hold the mouse over any name to see the type TypeScript inferred for it.

Three ways to run TypeScript

Node.js was built to run JavaScript. There are three ways to get from report.ts to a running program, and you will see all three in real projects.

1. tsx: run it directly

You already did this: npx tsx report.ts. tsx removes the types in memory and hands the JavaScript to Node.js. It is fast, understands every TypeScript feature, and is what this course uses while you develop.

2. node: built-in type stripping

Node.js 24 can run many .ts files itself. It uses a feature called type stripping: it replaces the types with spaces and runs what is left.

Terminal on your computer
node --version
v24.19.0
node report.ts
#1 Buy milk (open)

This only works for erasable syntax: TypeScript where deleting the types leaves valid JavaScript. A few older TypeScript features generate code instead, and Node.js refuses them. An enum is one:

status.ts
enum Status {
  Todo,
  Done,
}

console.log(Status.Done);
Output of npx tsx status.ts and of the browser terminal
1
Terminal on your computer
npx tsx status.ts
1
node status.ts
node:internal/modules/run_main:107
    triggerUncaughtException(
    ^

file://~/ts-tasks/status.ts:1
  > enum Status {
      Todo,
      Done,
  > }

SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
    at parseTypeScript (node:internal/modules/typescript:68:40)

Node.js v24.19.0

The limits of node file.ts:

  • It does not check types. Neither does tsx. Only tsc checks.
  • Erasable syntax only. No enum, no namespace, no parameter properties (you will meet those in Classes in TypeScript). This course avoids enum; a union of strings does the same job, as you will see.
  • It ignores tsconfig.json.
  • Imports must name the .ts file. tsx and tsc accept import … from "./store.js" for a file called store.ts; node does not. Modules in TypeScript explains why the course writes .js.

UNKNOWN FILE EXTENSION .TS?

Some Linux distributions build their own Node.js package without type stripping. There, node report.ts fails with Unknown file extension ".ts". Install Node.js from nodejs.org, or use tsx, which works everywhere.

3. tsc: compile to JavaScript

For production, you usually compile once and run plain JavaScript: nothing has to remove types while the server starts, and you run exactly the files you tested. Turn noEmit off for one run and tell tsc where to write the output:

Terminal on your computer
npx tsc --noEmit false --outDir dist
ls dist
report.js
cat dist/report.js
const task = { id: 1, title: "Buy milk", done: false };
function report(task) {
    return `#${task.id} ${task.title} (${task.done ? "done" : "open"})`;
}
console.log(report(task));
export {};
node dist/report.js
#1 Buy milk (open)

Look at dist/report.js. The type Task block is gone, and so are : Task and : string. What is left is the JavaScript you started with, with the typo fixed. The last line, export {};, keeps the file an ES module even though it exports nothing. tsc refuses to write output only when you ask it to (noEmitOnError); by default it writes the JavaScript even when there are type errors, so always read what it prints.

CommandChecks types?Use it for
npx tsc --noEmitYesChecking, before every commit and in CI
npx tsx file.tsNoRunning while you develop
node file.tsNoQuick scripts with erasable syntax only
npx tsc + node dist/file.jsYesBuilding for production

Types disappear when the code runs

You just saw it in dist/report.js: types exist only for the compiler. That has two consequences:

  • Running does not check. tsx and node will happily run report.ts with the typo in it and print undefined. Run npx tsc --noEmit before you trust your code.
  • The browser terminal on this page does not check either. It removes types the same way tsx does. Type errors only show on your computer, with npx tsc --noEmit, or in your editor.

TIP

VS Code runs the TypeScript checker as you type. The typo above gets a red squiggle the moment you write it, which is the fastest feedback of all.

There is one more consequence, and it matters a lot for a backend: types cannot check data that arrives while the program is running, such as a request from a user. TypeScript and JavaScript together, at the end of this part, deals with that.

Practice

TRY IT YOURSELF

Let the compiler find three bugs

This file has three mistakes. Save it as bugs.ts in your project and run npx tsc --noEmit. Read each error, fix it, and run the file with npx tsx bugs.ts.

bugs.tsNode.js only
type Task = {
  id: number;
  title: string;
  done: boolean;
};

const task: Task = { id: "1", title: "Buy milk", done: false };

function complete(task: Task): Task {
  return { ...task, done: "yes" };
}

console.log(complete(task, true).title);
What npx tsc --noEmit prints
bugs.ts:7:22 - error TS2322: Type 'string' is not assignable to type 'number'.

7 const task: Task = { id: "1", title: "Buy milk", done: false };
                       ~~

  bugs.ts:2:3 - The expected type comes from property 'id' which is declared here on type 'Task'
    2   id: number;
        ~~

bugs.ts:10:21 - error TS2322: Type 'string' is not assignable to type 'boolean'.

10   return { ...task, done: "yes" };
                       ~~~~

  bugs.ts:4:3 - The expected type comes from property 'done' which is declared here on type 'Task'
    4   done: boolean;
        ~~~~

bugs.ts:13:28 - error TS2554: Expected 1 arguments, but got 2.

13 console.log(complete(task, true).title);
                              ~~~~


Found 3 errors in the same file, starting at: bugs.ts:7
Show a solution
bugs.ts
type Task = {
  id: number;
  title: string;
  done: boolean;
};

const task: Task = { id: 1, title: "Buy milk", done: false };

function complete(task: Task): Task {
  return { ...task, done: true };
}

console.log(complete(task).title);
Output of npx tsx bugs.ts and of the browser terminal
Buy milk

TRY IT YOURSELF

Compile it and read the JavaScript

Compile your fixed bugs.ts to a dist folder and run the result with node. Before you open dist/bugs.js, guess which lines of your file will be different in it.

Show a solution
Terminal on your computer
npx tsc --noEmit false --outDir dist
node dist/bugs.js
Buy milk

In dist/bugs.js, the whole type Task block is gone, and so are the three annotations: : Task after task, and : Task twice in the complete line. Everything else is the same code you wrote.

TRY IT YOURSELF

Which one is not erasable?

Two of these lines run with node file.ts and one does not. Which one, and why?

const limit: number = 10;
enum Priority { Low, High }
function double(n: number): number { return n * 2; }
Show a solution

The enum. Deleting the types from the other two lines leaves valid JavaScript (const limit = 10; and function double(n) { return n * 2; }). An enum has no JavaScript equivalent, so TypeScript must generate an object for it, and Node's type stripping does not generate code. Use tsx, or write a union of strings instead: type Priority = "low" | "high".

Recap

  • TypeScript adds types to JavaScript. The compiler, tsc, uses them to catch mistakes before the code runs.
  • npm install -D typescript tsx @types/node, plus a tsconfig.json with strict on, sets up a project.
  • Annotate function parameters; let TypeScript infer the rest.
  • npx tsc --noEmit checks. npx tsx file.ts runs. node file.ts runs erasable TypeScript. tsc without noEmit compiles to .js. Only tsc checks.
  • Types are removed before the code runs, so they cannot check data that arrives at runtime.

Test yourself

Five questions, picked at random from this lesson's question bank. Some ask you to choose an answer, some to predict what code prints, and some to write code and run it in the terminal. Get 4 of 5 right to pass. If you don't, read the explanations and try again: you get 5 different questions.