Mert Özen Further With Every Line
Frontend TypeScript Basics — #02

Setting Up TypeScript: Running Code Is Not the Same as Type-Checking It

Mert Özen Nov 3, 2026 7 min 0 views
Setting Up TypeScript: Running Code Is Not the Same as Type-Checking It

The TypeScript Playground, installing with npm, tsc --init, npx tsc --noEmit and node file.ts on Node 26: why running code doesn't mean type-checking it, and how to read your first error message.

Version note: The code in this post targets TypeScript 7.0 and Node 26.

Predict First

In the previous post we saw TypeScript catch a wrong argument before the code ever ran. Now we're going to bring the same idea to your own machine. Say you have a file called hello.ts that looks like this:

function greet(name: string): string {
  return `Hello, ${name}!`;
}

console.log(greet(42));

greet expects a string, and we're handing it a number. What happens if you run this file in the terminal with node hello.ts? Does Node show a TypeScript error, or does it print something to the console and carry on? Keep your guess in mind; we'll answer it in the section where we run the code.

The Fastest Route: The TypeScript Playground

If you want to start without installing anything, typescriptlang.org/play is all you need. You write your code on the left, and broken lines get a red underline immediately. The panel on the right has a few tabs: the ".JS" tab shows your code as JavaScript with the types removed, and the ".D.TS" tab shows the declaration file produced from your code. A declaration file is like a summary made only of types; we'll look at it in detail in the Intermediate series. The "Errors" tab lists the errors, and the "Run" button runs the code in the browser and shows the console.log output in the "Logs" tab.

My favourite thing about the Playground is that it's shareable. The link in the address bar carries the code you wrote. When you want to ask a friend about an error, you send that link instead of a screenshot, and they open the same code with the same settings.

One note: in the version menu at the top of the Playground, the newest stable release is currently the 6.0 line, because the Playground still uses the compiler written in JavaScript. For the fundamentals, 6.0 and 7.0 behave the same and share the same defaults, so the examples in this series give the same result in both places.

Installing TypeScript in a Project

Real projects live on your own computer. Let's open an empty folder and install TypeScript as a dependency of this project:

mkdir ts-sandbox
cd ts-sandbox
npm init -y
npm i -D typescript
npx tsc -v

The last command prints a line like Version 7.0.2. npm i -D typescript now installs the new compiler written in Go; the command is still called tsc.

Why not a global install (npm i -g)? Because every TypeScript version behaves a little differently. When the version is written into package.json, everyone who clones the project works with the same compiler. npx tsc finds and runs the compiler in the project's node_modules folder. You may still see older sources recommending a global install; there's no benefit to it today.

A Lean tsconfig with tsc --init

The tsconfig.json file tells the compiler which rules to follow. You don't have to write it by hand; this command generates it for you:

npx tsc --init

The resulting file contains plenty of explanatory comment lines. Once we strip the comments, these are the settings that remain:

{
  "compilerOptions": {
    "module": "nodenext",
    "target": "esnext",
    "types": [],
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "strict": true,
    "jsx": "react-jsx",
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUncheckedSideEffectImports": true,
    "moduleDetection": "force",
    "skipLibCheck": true
  }
}

You don't need to memorize all of it now. For the moment, know three things. strict: true is the most important line; it turns on TypeScript's strict checking rules as a group, and this series runs in that mode from start to finish. module and target decide which JavaScript version and module system your code is interpreted against. noUncheckedIndexedAccess and exactOptionalPropertyTypes are two extra rules that go beyond strict; we'll touch on both in the array and object posts. We'll go through every line of this file in the post dedicated to tsconfig.

Type Checking: npx tsc --noEmit

Now put hello.ts in the folder and run the compiler:

npx tsc --noEmit

--noEmit means "just check, don't produce files." The compiler finds tsconfig.json, reads the project's .ts files, and reports this:

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

5 console.log(greet(42));
                    ~~


Found 1 error in hello.ts:5

If there are no errors, the command prints nothing and quietly finishes. In the TypeScript world, silence is good news. When there are errors, the command also exits with a non-zero exit code, which lets you stop code with type errors from moving forward in automated pipelines such as CI.

Running: node hello.ts

Node 26 can run .ts files directly. The technique behind it is "type stripping": Node reads the file, deletes the type annotations, and runs the JavaScript that's left. No extra package or setting is needed:

node hello.ts

Here's the answer to your guess: the console shows Hello, 42!. Node shows no error at all, because it doesn't check types; it only removes them. Once the annotations are gone, what's left is a greet(42) call, and JavaScript has no problem putting a number inside a template literal.

This is the real lesson of the post: code that runs is not code whose types are correct. Running and type checking are two separate jobs done by two separate tools. node hello.ts runs the code, npx tsc --noEmit checks the types. In a real project you use both. The split has an upside too: type checking reads the whole project and can take a while, while running starts instantly.

The fact that Node can only strip types has one more consequence. TypeScript has a few special pieces of syntax that don't leave JavaScript behind when you delete them; enum is the best-known example. Node can't strip those, so it throws an error. That's why we write the examples in this series using only strippable syntax; the exceptions get their own posts, where we explain the difference. The Node side of the details belongs to the Node series.

Reading Your First Error Message

Let's go back to the output above, because a good part of your time with TypeScript will be spent reading these messages. hello.ts:5:19 is the file name, line and column: the 19th character of line 5, which is where 42 starts. Next comes TS2345, the error's code. Every kind of error has a fixed number; search for that code and you'll find hundreds of people who hit the same error.

Then comes the message itself: "Argument of type 'number' is not assignable to parameter of type 'string'." You'll see this pattern a lot. It first tells you the type of what you passed (number), then the type that was expected (string). Below it is the offending line itself, with ~~ marks underneath pointing at the problem. At the very end is the total number of errors.

When you redirect the output to a file or run it in CI, the colours and the code snippet go away and the message collapses to a single line: hello.ts(5,19): error TS2345: …. The format differs, but the information is the same.

Editor Support

In practice you'll see most errors in the editor, not in the terminal. VS Code ships with TypeScript support; open your .ts file without installing any extension and the broken line gets a red underline. Hover over the underline and you see the same message you saw in the terminal. Hover over a variable and it shows which type TypeScript found for it; we'll use that feature a lot in the next post.

The editor checks types as you type, but it focuses only on the files you have open, and the TypeScript version it carries can differ from the one in your project. So npx tsc --noEmit always has the final word. The editor is the helper sitting next to you while you write; tsc is the last check at the door.

Common Pitfalls

The first pitfall shows up when you want to check a single file. If you type npx tsc hello.ts while there's a tsconfig.json in the folder, you get this error:

error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error.

This command used to silently ignore tsconfig and compile the file with default settings, and people couldn't understand why they were seeing different errors. Now it fails loudly. If you want to check the whole project, type npx tsc --noEmit without a file name. If you really want to check a single file independently of tsconfig, say so explicitly:

npx tsc --noEmit --ignoreConfig hello.ts

The second pitfall is forgetting --noEmit. If you type just npx tsc, the compiler produces files on top of checking; with our tsconfig, hello.js, hello.d.ts and two .map files appear next to hello.ts. It produces them even when there are errors. As long as we run the code directly with Node, we don't need those outputs.

The third, and the most dangerous: thinking "Node ran it, so it must be right." As you just saw, Node also runs code that has type errors. Before running a file, either look at the red underlines in your editor or run npx tsc --noEmit.

A Small Experiment

★☆☆ Follow the steps in this post on your own computer: create the folder, install TypeScript, run tsc --init, and try hello.ts with both npx tsc --noEmit and node hello.ts. Put the output of the two commands side by side.

★★☆ Predict, then try: if you write greet("Ada", "Grace") instead of greet(42), what does npx tsc --noEmit say, and what does node hello.ts print? Note down the error code and the column number.

★★☆ Add the line "check": "tsc --noEmit" to the scripts section of package.json and run it with npm run check. Look at how the command ends with and without an error.

★★★ Paste the same hello.ts into the Playground, copy the link and open it in a new tab. Then compare the output in the ".JS" tab with the code Node runs: what's left once the types are removed?

The environment is ready. We can now both run and check. In the next post we'll make full use of that "hover to see the type" feature in the editor: how TypeScript works out a variable's type on its own, when we should write an annotation, and when we shouldn't.