TypeScript From Zero
Why TypeScript, and Your First Project
What TypeScript is and is not, why types are erased at runtime, setting up a project, a tsconfig explained line by line, and reading your first compiler errors.
On this page
In one line: TypeScript is JavaScript plus a type checker that runs before your code runs. The types are checked and then erased, so the program that runs is plain JavaScript and the types do nothing at runtime. Learn that one fact first, and most of the rest makes sense.
You already know some JavaScript, or you are learning it. This series, "TypeScript From Zero to Confident", adds types to it one small step at a time. This first lesson does not teach many type rules yet. It sets up a working project, explains every line of a small config file, and shows you how to read the first error messages, so that the next lessons have a place to run.
If you want interview practice later, read Full-Stack Q&A, Lesson 3 for the common TypeScript questions and Frontend Depth, Lesson 4 for the advanced ones. This series builds the base under both.
What I ran / checked. I installed the current stable release from npm in an empty folder: TypeScript 7.0.2 (tsc -v prints Version 7.0.2), tsx 4.23.15 and @types/node 22.20.5, on Node v22.22.0 (npm 10.9.4). Every code block and every error message in this lesson comes from running those versions. I also read (not ran) the TypeScript 6.0 release notes and the Node.js "Modules: TypeScript" page from their GitHub sources. Error text is quoted exactly as tsc --pretty false printed it. If your version is different, the numbers after TS stay the same for most errors, but the wording can change a little.
Words used in this lesson
- A type is a label for what kind of value something is, such as
string(text) ornumber. - A type checker is a tool that reads your code and reports places where a value is used in a way its type does not allow. It does not run the code.
- A compiler turns code from one form into another.
tscis the TypeScript compiler. It checks types and can write out JavaScript. - Transpiling means turning TypeScript into JavaScript. Here that mostly means deleting the types.
- Type erasure (or "type stripping") means the types are removed from the code and do not exist when the program runs.
- A runtime is the place where code runs, such as Node.js or the browser.
tsconfig.jsonis the settings file that tellstscwhich files to read and how strict to be.- Strict mode (the
strictoption) is a group of checks that catch more mistakes. It is not the same as JavaScript's"use strict".
What TypeScript is, and what it is not
TypeScript is a superset of JavaScript. That means every valid JavaScript program is also a valid TypeScript program, and TypeScript adds more syntax on top: mainly the : type notes.
function greet(name: string): string { return "Hello, " + name;}console.log(greet("Ada"));The first : string says "the parameter is text"; the second says "the function returns text". I saved this as src/hello.ts and ran it:
$ node src/hello.tsHello, AdaTypeScript is:
- A way to write down what shape your data has, so the editor and the compiler can warn you before you run anything.
TypeScript is not:
- A different language at runtime. The browser and Node never see types.
- A guard against bad data coming from outside. A JSON response from a server can say anything. The type checker cannot know. (Lesson 6 covers checking data at runtime.)
- A promise that nothing can go wrong. It finds one group of mistakes (wrong shapes, missing values, typos). Logic bugs stay.
Types are erased
Let us see this with our own eyes. First a file with types, src/erase.ts:
type Id = string | number;interface User { id: Id; name: string }const u: User = { id: 1, name: "Ada" };console.log(u);I asked tsc to write the JavaScript (--noEmit false --rootDir src --outDir dist; more on these flags below) and printed the result, dist/erase.js:
const u = { id: 1, name: "Ada" };console.log(u);export {};The type line and the interface line are gone. The : User note is gone. (The export {} line is how tsc marks the file as a module. It does nothing here.) That is erasure. The consequence is the most important idea in this lesson: a type can only protect you while tsc is checking. At runtime there is no User.
Here is a program where the types lie, src/rt.ts:
const data = JSON.parse('{"id": "abc"}') as { id: number };console.log(typeof data.id, data.id.toFixed(2));tsc reported no errors. as { id: number } is a type assertion: you tell the checker "trust me, this is that shape". The checker believed it. Then I ran it:
$ node src/rt.tsTypeError: data.id.toFixed is not a functionThe value was the text "abc", and text has no toFixed. The type said number and the data said string, and at runtime the data wins. Keep this in mind whenever you reach for as.
Types describe what you believe, not what is true. TypeScript checks that your code agrees with the types you wrote. It cannot check that the outside world agrees with them. Data from fetch, JSON.parse, forms, files and databases needs a runtime check.
Setting up: three ways to run TypeScript
You need Node.js 22 or newer. Check with node -v. I used v22.22.0.
Make a folder and start a project:
mkdir hello-ts && cd hello-tsnpm init -ynpm install --save-dev typescript @types/nodetypescript gives you the tsc command. @types/node gives TypeScript the type information for Node's own functions, such as process and fs (without it, TypeScript does not know what process is; there is an example below). Then open package.json and add "type": "module" so that .ts files may use import and export. Mine looked like this:
{ "name": "hello-ts", "private": true, "type": "module" }(plus the devDependencies that npm added.) There are three tools to know. They do different jobs, and people mix them up.
| Tool | Checks types? | Runs the code? | Typical use |
|---|---|---|---|
tsc | Yes | No (it can write .js files) | Type checking, builds, CI |
tsx | No | Yes | Running scripts and dev servers quickly |
node file.ts (type stripping) | No | Yes | Running simple scripts with nothing extra installed |
1. tsc: the checker
npx tscWith the tsconfig in the next section, a clean project prints nothing and exits with code 0. I made a wrong version of hello.ts that calls greet(42), and tsc printed:
src/bad.ts(5,19): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.and exited with code 1. (npx runs a program that is installed in your project's node_modules.)
2. tsx: run it now
npm install --save-dev tsxnpx tsx src/hello.tstsx strips the types and runs the file. It also understands more TypeScript than Node does. It does not check types. I ran the bad greet(42) program with tsx:
$ npx tsx src/bad.tsHello, 42No error. So tsx and tsc go together: tsx to run, tsc to check.
3. Node's built-in type stripping
Since Node v22.18.0 you can run a .ts file directly and no flag is needed (the Node docs list "type stripping is enabled by default" for that version, and I confirmed it on v22.22.0):
$ node src/hello.tsHello, AdaNode deletes the types (it replaces them with spaces) and runs what is left. It does not type check, and it does not read tsconfig.json. The same bad program printed Hello, 42 under plain node.
Because Node only deletes, it cannot run TypeScript that needs to be rewritten into JavaScript. I tested an enum:
enum Color { Red, Green }console.log(Color.Green);$ node src/enum.tsSyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only modenpx tsx ran the same file and printed 1. A class parameter property (constructor(public name: string) {}, covered in Lesson 2) fails in the same way: TypeScript parameter property is not supported in strip-only mode. The Node docs name these as features that need transformation: enums, namespace with runtime code, parameter properties and import aliases. With node --experimental-transform-types they work, and Node prints an ExperimentalWarning. Decorators are not supported at all.
Two more rules from the Node docs, which I also hit:
- In an
import, write the real file name with the.tsextension:import { x } from "./types.ts". - Import types with the
typekeyword (details below). Otherwise Node tries to import something that does not exist at runtime.
Which one should I use? To learn: tsc to check, and node or tsx to run. In a real app your framework (Next.js, Vite) strips the types, and you run tsc --noEmit in CI. Node's type stripping is marked "Stability: 1.2 - Release candidate" in its docs.
A minimal tsconfig.json, line by line
tsc --init creates a starting file. In 7.0.2 it wrote module: nodenext, target: esnext, types: [], strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes, verbatimModuleSyntax, isolatedModules, skipLibCheck and more, with comments. I wrote a smaller one by hand for this series:
{ "compilerOptions": { "target": "es2022", "module": "nodenext", "strict": true, "noUncheckedIndexedAccess": true, "skipLibCheck": true, "isolatedModules": true, "verbatimModuleSyntax": true, "erasableSyntaxOnly": true, "allowImportingTsExtensions": true, "noEmit": true, "types": ["node"] }, "include": ["src"]}I used exactly this file for every example. Line by line:
Defaults changed in TypeScript 6.0. The 6.0 release notes say: strict is now true by default, module defaults to esnext, target defaults to the current-year ES version (es2025 in 7.0.2, which is what tsc --help --all shows), and types defaults to []. Old tutorials assume older defaults. I still write strict out in full, because a setting you can see in the file is a setting you can find.
"target": "es2022". Which version of JavaScript the output may use.tscalso uses it to decide which built-in features (likeArray.prototype.at) exist. Choose a version your runtime supports. Node 22 supportses2022and newer."module": "nodenext". Howimportandexportwork.nodenextfollows Node's real rules: ES modules or CommonJS, decided bypackage.json"type"and by the file extension; relative imports need a file extension. It also setsmoduleResolution(how TypeScript finds the file behind an import) tonodenext. I confirmed that withtsc --showConfig. If you use a bundler such as Vite, you would choose"module": "preserve"and"moduleResolution": "bundler"instead (Lesson 5)."strict": true. Turns on a family of checks. The most important ones:strictNullChecks(nullandundefinedare separate types, not allowed everywhere) andnoImplicitAny(you may not leave a type out and getanyby accident). Always use it for new code."noUncheckedIndexedAccess": true. Not part ofstrict. When you readarray[0]orobject[key], the result type gets| undefinedadded, because the item might not be there. It is a little annoying and catches real bugs. I show an example below."skipLibCheck": true. Do not type check the.d.tsfiles (type-only files) insidenode_modules. Other people's libraries sometimes disagree with each other, and you cannot fix them. The default isfalse; I confirmed that intsc --help --all. Skipping the check makes builds faster and removes noise. The cost is that a mistake in a library's own types is not reported."isolatedModules": true. Makes TypeScript warn about code that a tool which processes one file at a time cannot handle. Tools such astsx, esbuild, Babel and Node's stripping never see your other files, so they cannot know ifUseris a type or a value. This flag reports code that depends on that knowledge."verbatimModuleSyntax": true. A strict rule: what you write inimportandexportis what is kept in the output. Imports that are only types must saytype. This is easy for tools to follow, and it matches Node's stripping. Example below."erasableSyntaxOnly": true. TypeScript refuses syntax that Node's stripping cannot run (enums, parameter properties, runtime namespaces). The flag is in the Node docs' recommended settings. It makestsctell you about the problem before Node does."allowImportingTsExtensions": true. Lets you writefrom "./types.ts". TypeScript only allows this when it is not writing JavaScript, which is whynoEmitis next."noEmit": true.tsconly checks and does not write.jsfiles. Another tool (Node, tsx, a bundler) runs the code."types": ["node"]. Which global type packages to load. With["node"], names likeprocessare known. If you leave it out in 7.0.2, the list is empty. I tried"types": []and a file that usesprocess:
src/proc.ts(1,13): error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig."include": ["src"]. Only look at files insrc.
To write JavaScript files instead of only checking, remove noEmit and allowImportingTsExtensions and add "rootDir": "src" and "outDir": "dist". Without rootDir, 7.0.2 stops with error TS5011: The common source directory of 'tsconfig.json' is './src'. The 'rootDir' setting must be explicitly set ....
One more thing: if you run tsc src/main.ts while a tsconfig.json exists, it stops with error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error. Run npx tsc with no file names to use your config.
verbatimModuleSyntax in action
I made src/types.ts:
export interface User { id: number; name: string }export function makeUser(name: string): User { return { id: 1, name }; }and a src/main.ts that imports both names the normal JavaScript way:
import { makeUser, User } from "./types.ts";src/main.ts(1,20): error TS1484: 'User' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.At runtime, Node has no User to import:
SyntaxError: The requested module './types.ts' does not provide an export named 'User'The fix is to mark type-only names with type:
import { makeUser, type User } from "./types.ts";const u: User = makeUser("Ada");console.log(u.name);This compiled cleanly, and node src/main.ts printed Ada.
If you forget the extension (from "./types"), tsc says:
src/main2.ts(1,26): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './types.js'?(The .js hint is right if you build to JavaScript first. With the setup used here, write .ts.)
Re-exporting a type needs export type too. For export { User } from "./types.ts"; I got error TS1205: Re-exporting a type when 'verbatimModuleSyntax' is enabled requires using 'export type'. And the earlier enum Color file gives error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled. under this config.
Reading your first compiler errors
An error from tsc has the same parts every time:
src/e1.ts:2:18 - error TS2339: Property 'nmae' does not exist on type '{ name: string; age: number; }'.2 console.log(user.nmae); ~~~~(That is the --pretty format; I removed the colours. With --pretty false you get the one-line version src/e1.ts(2,18): error TS2339: ....)
src/e1.ts:2:18: file, line 2, column 18. The~~~~marks the exact word.TS2339: the error code. It is stable. Search for the code plus a few words, and you will find explanations.- The message after it says what is wrong. Read it as "I expected X but found Y".
Some messages have extra indented lines under them. Read the last indented line first, because it usually holds the real reason. Here are the errors I got most often, from small files. Each file uses the tsconfig above.
A typo in a property name.
const user = { name: "Ada", age: 36 };console.log(user.nmae);src/e1.ts(2,18): error TS2339: Property 'nmae' does not exist on type '{ name: string; age: number; }'.The message shows the type TypeScript worked out for user. You never wrote it: TypeScript inferred it (Lesson 1).
A parameter without a type.
function double(n) { return n * 2;}src/e2.ts(1,17): error TS7006: Parameter 'n' implicitly has an 'any' type.any means "turn the checker off for this value". noImplicitAny (part of strict) refuses to let you get it by accident. Fix: function double(n: number).
A value that might be missing.
function find(names: string[], q: string) { return names.find((n) => n === q);}const hit = find(["Ada", "Bo"], "Ada");console.log(hit.toUpperCase());src/e4.ts(5,13): error TS18048: 'hit' is possibly 'undefined'.Array.find returns undefined when nothing matches. strictNullChecks makes TypeScript remember that. Fix: check first, if (hit) { ... }. (Lesson 1 teaches this, called narrowing.)
An indexed read.
function firstWord(text: string): string { const words = text.split(" "); return words[0];}src/e3.ts(3,3): error TS2322: Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'.This one comes from noUncheckedIndexedAccess. I removed that line from the config and ran again: the error disappeared. An empty array has no words[0], so the check is correct about the risk.
Often one early mistake causes several later errors, and tsc reports all of them in one run. Fix the first one on the list, run again, and repeat.
What strict bought us
I ran the same files with "strict": false. The typo error (TS2339) stayed, because it does not depend on strict. The any parameter (TS7006), the possibly 'undefined' error (TS18048) and the string | undefined error (TS2322) all disappeared, because those checks live in strict. The program is not safer; the checker just stopped looking. That is why the setting stays on.
Do not silence an error you do not understand. as any, // @ts-ignore or turning a check off makes the red line go away and the bug stay. Read the last indented line of the message first.
Try it
- Create a project as shown above. Write
src/hello.tswith a functionadd(a: number, b: number): number. Run it withnode, withtsx, and check it withtsc. - Call
add("1", 2). Runtsc, then run the file withnode. What happens in each case? - Add
enum Dir { Up, Down }to a file. What doestscsay witherasableSyntaxOnly? What doesnodesay? Then change it toconst Dir = { Up: 0, Down: 1 } as const;(Lesson 2 explains this). - Move an
interfaceto another file and import it without the wordtype. Read both thetscerror and the Node error.
Answers.
- All three work.
tscprints nothing and exits with0. tscreportserror TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.noderuns the file anyway and"1" + 2gives the text"12". Types are erased, so Node never sees the problem.tsc:TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.Node:ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX ... TypeScript enum is not supported in strip-only mode. The object withas constis plain JavaScript plus a type note, so both accept it.tsc:TS1484. Node:The requested module './types.ts' does not provide an export named 'Name'.
Interview questions
What is TypeScript, in simple words? (Must know)
I say it is JavaScript with a type checker in front of it. I write type notes, the compiler checks them, and then the notes are removed. What runs in the browser or Node is plain JavaScript. So TypeScript helps me find mistakes while I write, but it adds nothing at runtime.
What does "types are erased" mean, and why does it matter? (Must know)
It means the output JavaScript has no types in it. interface, type and : string notes are deleted. It matters because I cannot check a type at runtime with typeof User or instanceof for an interface, and because data from outside (JSON, forms, APIs) is not checked just because I wrote a type for it. For that I need runtime validation, for example with a library like Zod.
What is the difference between tsc, tsx and running node file.ts? (Must know)
tsc is the real compiler and the only one of the three that checks types. tsx and node file.ts run the file by stripping the types and do not check them. I tested a call with the wrong argument type: tsc failed with TS2345, and tsx and node both ran it. In a real project I use one of the runners to run and tsc --noEmit in CI to check.
What are the limits of Node's built-in type stripping? (Must know)
It only deletes types, so it cannot run syntax that needs a rewrite: enums, namespaces with runtime code, and class parameter properties give ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX. It ignores tsconfig.json, so paths do not work. Imports need real .ts extensions, and type-only imports need the type keyword. It does not type check, and it refuses to run TypeScript files inside node_modules.
What does strict do, and what changed in TypeScript 6 and 7? (Must know)
strict turns on a group of checks. The two I name first are strictNullChecks and noImplicitAny. Since TypeScript 6.0, strict is true by default, module defaults to esnext and types defaults to an empty list. I still write strict: true in my config so it is visible and does not depend on the version.
What does noUncheckedIndexedAccess do? Why is it not in strict? (Senior signal)
It adds | undefined to the type of array[i] and object[key], because the item may not exist. It catches real bugs, such as reading the first item of an empty array. It is not in strict, I believe because it makes a lot of existing code report errors, so it is opt-in. In my test, return words[0] as a string failed with TS2322 only when this was on. I turn it on for new projects.
What are isolatedModules and verbatimModuleSyntax for? (Senior signal)
Many tools (esbuild, Babel, tsx, Node) process one file at a time and cannot see whether an imported name is a type or a value. isolatedModules makes tsc report code that needs that knowledge. verbatimModuleSyntax goes further: imports and exports are kept exactly as written, so type-only names must use import type or { type X }. When I broke the rule, tsc said TS1484 and Node failed with "does not provide an export named".
What does skipLibCheck do and what is the trade-off? (Must know)
It skips type checking of all .d.ts files, including the ones inside node_modules. Builds are faster, and I do not get errors from library types that I cannot fix. The cost is that an error inside a library's own type files is hidden. I use it in almost every app, and I would turn it off when I publish a library and want to check my own .d.ts files.
Why can as make a program crash even though tsc passes? (Must know)
as is an instruction to the checker, not a conversion. In my test I wrote JSON.parse(...) as { id: number }, the data really had "abc", tsc printed nothing, and Node crashed with TypeError: data.id.toFixed is not a function. Use as only when I know something the checker cannot, and validate outside data at runtime.
Key takeaways
- TypeScript = JavaScript + a checker. The types are erased; they do not exist at runtime.
tscchecks.tsxandnode file.tsrun. Onlytscfinds type errors.- Node 22 strips types by default (v22.18.0 and later). It cannot run enums, parameter properties or runtime namespaces, and it ignores
tsconfig.json. - A good start config:
strict,noUncheckedIndexedAccess,skipLibCheck,isolatedModules,verbatimModuleSyntax,erasableSyntaxOnly,noEmitandtypes: ["node"]. - Read an error as: file, line, column, code, message. Read the last indented line first.
- Do not trust
as,anyor@ts-ignoreto make errors go away.
What's next
Lesson 1: Basic Types, Inference and Narrowing. Now that you can run tsc and read its messages, we learn the basic types, why you can often leave them out, and how TypeScript follows your if checks to know what a value is.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- Validation at the Edge: Schemas, DTOs and Trusting Nothing · Backend in Depth
- Advanced TypeScript for Interviews: Generics, Narrowing, Utility and Conditional Types · Senior Frontend Depth
- TypeScript Interview Questions · Full-Stack Questions
Part of the study guide
Related series