Skip to main content
TypeScript · lesson 2 of 21
TypeScript beginner Lesson 2 of 21

Setting Up TypeScript

Install TypeScript 7, read what tsc --init generates, run .ts files with Node's type stripping or tsx, and type-check them with tsc --noEmit.

By SkillByExample · Updated

Installing TypeScript

TypeScript should be installed as a project dev dependency rather than globally. This keeps the version pinned to your project, so everyone on the team uses the same compiler. Every command here was run with TypeScript 7.0.2 on Node v24.18.1, in a new folder created with npm init -y.

$ npm install -D typescript

added 2 packages, and audited 3 packages in 1s

found 0 vulnerabilities
$ npx tsc --version
Version 7.0.2

npx tsc runs the compiler from node_modules/.bin. Version 7 is the native compiler written in Go, so the two packages npm added are typescript itself and a platform-specific binary (@typescript/typescript-win32-x64 on this Windows machine). You can install it globally with npm install -g typescript, but then each project uses whatever version that machine happens to have.

Your First Compile

Before adding a tsconfig.json, you can compile a single file directly. Create hello.ts:

// Type annotations are stripped during compilation
const greeting: string = "Hello, TypeScript!";
console.log(greeting);
$ npx tsc hello.ts
$ node hello.js
Hello, TypeScript!
$ node hello.ts
Hello, TypeScript!

tsc printed nothing, which means no errors, and wrote hello.js next to the source. Node 24 can also run hello.ts itself, because Node 22.18 and later strip type annotations before running a file. That is a convenience for running code, not a type check, as Running TypeScript During Development shows.

tsconfig.json

For any real project you need a tsconfig.json. It tells the compiler where your source files live, what JavaScript to emit, and which strictness checks to apply. Generate one:

$ npx tsc --init

Created a new tsconfig.json

You can learn more at https://aka.ms/tsconfig
$ cat tsconfig.json
{
  // Visit https://aka.ms/tsconfig to read more about this file
  "compilerOptions": {
    // File Layout
    // "rootDir": "./src",
    // "outDir": "./dist",

    // Environment Settings
    // See also https://aka.ms/tsconfig/module
    "module": "nodenext",
    "target": "esnext",
    "types": [],
    // For nodejs:
    // "lib": ["esnext"],
    // "types": ["node"],
    // and npm install -D @types/node

    // Other Outputs
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,

    // Stricter Typechecking Options
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,

    // Style Options
    // "noImplicitReturns": true,
    // "noImplicitOverride": true,
    // "noUnusedLocals": true,
    // "noUnusedParameters": true,
    // "noFallthroughCasesInSwitch": true,
    // "noPropertyAccessFromIndexSignature": true,

    // Recommended Options
    "strict": true,
    "jsx": "react-jsx",
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUncheckedSideEffectImports": true,
    "moduleDetection": "force",
    "skipLibCheck": true,
  }
}

tsconfig.json allows comments and trailing commas, so this file is valid as generated. If you learned TypeScript before version 6, several of these values are new. module is nodenext, not CommonJS. target is esnext. And types is an empty list, which means @types/* packages are no longer picked up automatically.

Key tsconfig Options Explained

These options have the biggest effect on your day-to-day experience.

strict: true turns on a bundle of checks that catch real bugs, including:

  • strictNullChecks — null and undefined are not assignable to other types, so you have to handle them explicitly
  • noImplicitAny — a parameter whose type can’t be inferred must be annotated, which prevents accidental any
  • strictFunctionTypes — stricter checking of function parameter types

Since TypeScript 6, strict is also the default when there’s no tsconfig.json at all, which is why tsc rejects an unannotated parameter even for a single file.

target controls which JavaScript syntax tsc rewrites into older syntax. esnext rewrites nothing. TypeScript 7 removed ES5 (it reports error TS5108), so the oldest target left is ES2015.

module: "nodenext" follows Node’s own rules. A file is an ES module or a CommonJS module depending on the nearest package.json’s "type" field, and tsc emits import or require to match. The next section shows why that matters.

types: [] means Node’s built-in names such as process and node:fs aren’t typed until you install @types/node and set "types": ["node"], as the comment in the file says.

verbatimModuleSyntax and isolatedModules require every file to be compilable on its own, without looking at other files. That’s exactly what Node’s type stripping and tools like tsx and esbuild do, so the generated config keeps your code runnable by all of them.

noUncheckedIndexedAccess and exactOptionalPropertyTypes go beyond strict. The first makes arr[0] have type T | undefined. The second stops you from assigning undefined to an optional property that was declared without it.

Compiling a Project

Put the code in src/index.ts:

export function shout(message: string): string {
  return message.toUpperCase() + "!";
}

console.log(shout("hello from src/index.ts"));

Then uncomment the two “File Layout” lines in tsconfig.json, so they read "rootDir": "./src", and "outDir": "./dist",, and build. With no file names, tsc reads tsconfig.json:

$ npx tsc
error TS6059: File 'C:/tmp/tssetup/hello.ts' is not under 'rootDir' 'C:/tmp/tssetup/src'. 'rootDir' is expected to contain all source files.
  The file is in the program because:
    Matched by default include pattern '**/*'
  File is CommonJS module because 'C:/tmp/tssetup/package.json' has field "type" whose value is not "module"

Without an include list, tsc compiles every .ts file under the folder, so the old hello.ts is still part of the program, and it sits outside rootDir. Add "include": ["src"] after compilerOptions:

    "skipLibCheck": true,
  },
  "include": ["src"]
}
$ npx tsc
src/index.ts(1,1): error TS1287: A top-level 'export' modifier cannot be used on value declarations in a CommonJS module when 'verbatimModuleSyntax' is enabled.

The last line of the TS6059 error already gave the reason. npm init -y writes "type": "commonjs" into package.json, and with module: nodenext that makes src/index.ts a CommonJS file. Under verbatimModuleSyntax, tsc won’t quietly turn export into exports.shout = .... Declare the package as ES modules:

$ npm pkg set type=module
$ npx tsc
$ ls dist
index.d.ts
index.d.ts.map
index.js
index.js.map
$ node dist/index.js
HELLO FROM SRC/INDEX.TS!

One source file became four output files, because the generated config turns on declaration, declarationMap and sourceMap. Practice 2 looks inside them.

Running TypeScript During Development

Running tsc and then node on every change is slow. There are now two common ways to run a .ts file directly: Node itself, and tsx (npm install -D tsx).

$ node src/index.ts
HELLO FROM SRC/INDEX.TS!
$ npx tsx src/index.ts
HELLO FROM SRC/INDEX.TS!

Both print the same thing, and neither one type-checks (Practice 4 proves it). The difference is what they accept. Node only strips types: it deletes the annotations and runs what’s left. TypeScript syntax that generates JavaScript, such as an enum, can’t be stripped. Save this as src/status.ts:

enum Status {
  Active,
  Disabled,
}

console.log(Status.Disabled, Status[1]);
$ node src/status.ts
node:internal/modules/run_main:107
    triggerUncaughtException(
    ^

file:///C:/tmp/tssetup/src/status.ts:1
  > enum Status {
      Active,
      Disabled,
  > }

SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
    at parseTypeScript (node:internal/modules/typescript:68:40)
    at processTypeScriptCode (node:internal/modules/typescript:146:42)
    at stripTypeScriptModuleTypes (node:internal/modules/typescript:209:22)
    at ModuleLoader.<anonymous> (node:internal/modules/esm/translators:680:29)
    at #translate (node:internal/modules/esm/loader:434:20)
    at afterLoad (node:internal/modules/esm/loader:502:29)
    at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:507:12)
    at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:560:36)
    at afterResolve (node:internal/modules/esm/loader:607:52)
    at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:613:12) {
  code: 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX'
}

Node.js v24.18.1
$ npx tsx src/status.ts
1 Disabled

tsx compiles each file with esbuild instead of stripping it, so it handles enums, parameter properties and the other non-erasable syntax. Plain node is enough if you avoid that syntax, and tsx covers everything else.

Older guides recommend ts-node. With TypeScript 7 installed, the current release (ts-node 10.9.2) crashes before running anything:

$ npx ts-node src/index.ts
C:\tmp\tssetup\node_modules\ts-node\dist\configuration.js:91
    const { fileExists = ts.sys.fileExists, readFile = ts.sys.readFile, skipProject = index_1.DEFAULTS.skipProject, project = index_1.DEFAULTS.project, tsTrace = index_1.DEFAULTS.tsTrace, } = rawApiOptions;
                                ^

TypeError: Cannot read properties of undefined (reading 'fileExists')
    at readConfig (C:\tmp\tssetup\node_modules\ts-node\dist\configuration.js:91:33)
    at findAndReadConfig (C:\tmp\tssetup\node_modules\ts-node\dist\configuration.js:50:84)
    at phase3 (C:\tmp\tssetup\node_modules\ts-node\dist\bin.js:254:67)
    at bootstrap (C:\tmp\tssetup\node_modules\ts-node\dist\bin.js:47:30)
    at main (C:\tmp\tssetup\node_modules\ts-node\dist\bin.js:33:12)
    at Object.<anonymous> (C:\tmp\tssetup\node_modules\ts-node\dist\bin.js:579:5)
    at Module._compile (node:internal/modules/cjs/loader:1871:14)
    at Object..js (node:internal/modules/cjs/loader:2002:10)
    at Module.load (node:internal/modules/cjs/loader:1594:32)
    at Module._load (node:internal/modules/cjs/loader:1396:12)

Node.js v24.18.1

ts-node runs the TypeScript compiler through its JavaScript API, and ts.sys doesn’t exist in the TypeScript 7 package, whose lib/ folder holds only a small launcher for the native binary. Remove ts-node and use node or tsx.

A typical set of scripts runs the file directly during development, and keeps tsc for checking and building:

{
  "scripts": {
    "dev": "node --watch src/index.ts",
    "typecheck": "tsc --noEmit",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}
$ npm run build

> tssetup@1.0.0 build
> tsc

$ npm start

> tssetup@1.0.0 start
> node dist/index.js

HELLO FROM SRC/INDEX.TS!
$ npm run dev

> tssetup@1.0.0 dev
> node --watch src/index.ts

HELLO FROM SRC/INDEX.TS!
Completed running 'src/index.ts'. Waiting for file changes before restarting...

node --watch re-runs the file every time you save it, and stays running until you press Ctrl+C. Swap in tsx watch src/index.ts if your code uses enums or other syntax Node can’t strip.

Project Structure

Keeping source and compiled output in separate directories prevents confusion and makes it easy to gitignore the build artifacts. This is the standard layout most TypeScript projects follow:

my-project/
├── src/
│   ├── index.ts
│   ├── utils/
│   │   └── helpers.ts
│   └── types/
│       └── index.ts
├── dist/           # compiled output — add this to .gitignore
├── tsconfig.json
├── package.json
└── .gitignore

Add dist/ to .gitignore — compiled output is derived from source and should never be committed.

VS Code Setup

VS Code has TypeScript support built in, using a copy of TypeScript it bundles. For TypeScript 6 and earlier, the command palette’s TypeScript: Select TypeScript Version → Use Workspace Version points the editor at your project’s node_modules/typescript/lib/tsserver.js, so the editor and npx tsc agree. TypeScript 7 doesn’t ship that file:

$ ls node_modules/typescript/lib
getExePath.d.ts
getExePath.js
tsc.js
version.cjs
version.d.cts

So on TypeScript 7, the editor may report slightly different errors from your project’s tsc. Treat npx tsc --noEmit as the final word.

Recommended extensions that improve the TypeScript experience significantly:

  • Error Lens — shows type errors inline in the editor, right next to the code
  • Pretty TypeScript Errors — formats complex nested type errors into readable output

Extending a Base Config

The @tsconfig/bases project publishes shareable configs for common environments, so you don’t have to maintain every compiler option yourself. In a new folder with "type": "module" and the same src/index.ts, install the Node 24 base and extend it:

{
  "extends": "@tsconfig/node24/tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist"
  },
  "include": ["src"]
}
$ npm install -D typescript @tsconfig/node24
$ npx tsc
error TS2688: Cannot find type definition file for 'node'.
  The file is in the program because:
    Entry point of type library 'node' specified in compilerOptions
tsconfig.json(4,5): error TS5011: The common source directory of 'tsconfig.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.
  Visit https://aka.ms/ts6 for migration information.

Two errors, both new in TypeScript 6 and later. The base sets "types": ["node"], so it expects @types/node to be installed. And outDir now needs an explicit rootDir. Older versions guessed it from the source files, so src/index.ts would land at dist/index.js or at dist/src/index.js depending on which files existed. Install the types and add "rootDir": "./src" above outDir:

$ npm install -D @types/node@24
$ npx tsc
$ ls dist
index.js
$ node dist/index.js
HELLO FROM SRC/INDEX.TS!

The resolved versions were @tsconfig/node24@24.0.5 and @types/node@24.19.2. The base sets target: es2024 and module: nodenext for you, so your own tsconfig.json only needs the options specific to this project.

Running TypeScript 6 Alongside

Some tools still need TypeScript 6, and the TypeScript team publishes @typescript/typescript6, which provides a tsc6 command. Installed in the same project as TypeScript 7, it takes over tsc too:

$ npm install -D typescript @typescript/typescript6
$ npm ls --depth=0
tsboth@1.0.0 C:\tmp\tsboth
├── @typescript/typescript6@6.0.2
└── typescript@7.0.2

$ npx tsc --version
Version 6.0.3
$ npx tsc6 --version
Version 6.0.3

@typescript/typescript6 depends on a renamed copy of typescript@6, and npm linked that copy’s tsc into node_modules/.bin, overwriting TypeScript 7’s. Uninstalling it removes the link entirely, and npx tsc then downloads an unrelated package named tsc. To run TypeScript 6 once, without touching the project, ask npx for it:

$ npx tsc --version
Version 7.0.2
$ npx -p typescript@6 tsc --version
Version 6.0.3

Running the Build

These are the three compiler commands you will use day to day. --noEmit checks types without writing anything, and prints nothing when there are no errors:

$ npx tsc --noEmit; echo "exit code $?"
exit code 0

The exit code is what CI looks at: 0 with no errors, and non-zero as soon as there is one. npx tsc with no options does a full build into dist/, as above. --watch keeps running and rechecks on every save. Here a line was appended to src/index.ts while it ran (--preserveWatchOutput stops it clearing the screen between runs):

$ npx tsc --watch --preserveWatchOutput
10:39:12 PM - Starting compilation in watch mode...

10:39:12 PM - Found 0 errors. Watching for file changes.

10:39:15 PM - File change detected. Starting incremental compilation...

src/index.ts(6,7): error TS2322: Type 'string' is not assignable to type 'number'.
10:39:15 PM - Found 1 error. Watching for file changes.

The appended line was const count: number = "three";, and watch mode reported it within moments of the save. Leave this running in a terminal next to npm run dev, so one shows output and the other shows type errors.

Practice

The solutions were run with TypeScript 7.0.2 and tsx 4.23.15 on Node v24.18.1, in the project built up in this lesson (tsc --init config with rootDir, outDir and "include": ["src"], and "type": "module" in package.json).

  1. Add a line to hello.ts that breaks its own annotation: const count: number = "three";, and change the console.log to print greeting, count. Compile it with npx tsc hello.ts. The project now has a tsconfig.json, so read the error, follow its advice, then run node hello.js.
Solution
// Type annotations are stripped during compilation
const greeting: string = "Hello, TypeScript!";
const count: number = "three";
console.log(greeting, count);
$ npx tsc hello.ts
error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error.
$ ls hello.js
ls: cannot access 'hello.js': No such file or directory
$ npx tsc --ignoreConfig hello.ts
hello.ts(3,7): error TS2322: Type 'string' is not assignable to type 'number'.
$ node hello.js
Hello, TypeScript! three

Since TypeScript 6, naming a file in a folder that has a tsconfig.json is an error, and nothing is written. --ignoreConfig compiles the file with the default options instead. The type check fails, but tsc still emits JavaScript, because type errors don’t block output by default. Once the annotation is stripped, nothing stops Node from running the string.

  1. Build the project with npx tsc and look at what it wrote. Which files come from declaration, declarationMap and sourceMap, and what is in the .d.ts file?
Solution
$ npx tsc
$ ls dist
index.d.ts
index.d.ts.map
index.js
index.js.map
$ node dist/index.js
HELLO FROM SRC/INDEX.TS!
$ cat dist/index.d.ts
export declare function shout(message: string): string;
//# sourceMappingURL=index.d.ts.map

declaration writes index.d.ts, which holds the types without the function body. That’s what another TypeScript project reads when it imports this one. declarationMap writes index.d.ts.map, so “go to definition” in an editor lands in src/index.ts, not the .d.ts. sourceMap writes index.js.map, so stack traces and debuggers point at the TypeScript source.

  1. Remove the annotations from shout so it reads export function shout(message) {, delete dist/, and run npx tsc --noEmit. Then try turning the check off by changing "strict": true to "strict": false. If that doesn’t work, put strict back and pass --noImplicitAny false on the command line instead.
Solution
export function shout(message) {
  return message.toUpperCase() + "!";
}

console.log(shout("hello from src/index.ts"));

With "strict": true:

$ npx tsc --noEmit
src/index.ts(1,23): error TS7006: Parameter 'message' implicitly has an 'any' type.
$ ls dist
ls: cannot access 'dist': No such file or directory

With "strict": false:

$ npx tsc --noEmit && echo "no type errors"
tsconfig.json(25,5): error TS5052: Option 'exactOptionalPropertyTypes' cannot be specified without specifying option 'strictNullChecks'.

With "strict": true and --noImplicitAny false:

$ npx tsc --noEmit --noImplicitAny false && echo "no type errors"
no type errors
$ ls dist
ls: cannot access 'dist': No such file or directory

noImplicitAny, one of the flags strict turns on, refuses a parameter whose type it can’t infer. Turning all of strict off fails in this config, because the generated exactOptionalPropertyTypes only makes sense with strictNullChecks. Turning off just noImplicitAny lets message silently become any, and the check passes. In every case, --noEmit writes nothing to dist/.

  1. Keep the unannotated shout from task 3, with strict on. Run src/index.ts with node and with tsx, then run npx tsc --noEmit. Which of the three notices the problem?
Solution
$ node src/index.ts
HELLO FROM SRC/INDEX.TS!
$ npx tsx src/index.ts
HELLO FROM SRC/INDEX.TS!
$ npx tsc --noEmit
src/index.ts(1,23): error TS7006: Parameter 'message' implicitly has an 'any' type.

Only tsc. node and tsx both remove the types and run the JavaScript, without checking anything, so code that fails the strict check runs fine. That’s why the typecheck script exists: run it in CI, in a pre-push hook, or alongside npm run dev in a second terminal.

Frequently Asked Questions

Do I need to install TypeScript globally?
No. Installing it as a dev dependency in your project (npm install -D typescript) is the recommended approach. It keeps the version locked to the project and avoids conflicts between projects, and npx tsc runs that local copy.
Can I still use ts-node with TypeScript 7?
Not with ts-node 10.9.2, the current release. With typescript 7.0.2 installed it crashes before running anything, with TypeError: Cannot read properties of undefined (reading 'fileExists'), because TypeScript 7 no longer ships the JavaScript compiler API ts-node calls. Use node file.ts on Node 22.18 or later, or tsx, to run TypeScript directly.
Does node file.ts type-check my code?
No. Node's built-in TypeScript support only strips the type annotations and runs the JavaScript that is left, so code with type errors still runs. It also refuses syntax that can't simply be stripped, such as enums. Run tsc --noEmit to check types.
What does tsc --init generate in TypeScript 7?
A tsconfig.json with module nodenext, target esnext, types set to an empty list, strict, verbatimModuleSyntax, isolatedModules and skipLibCheck turned on, plus noUncheckedIndexedAccess, exactOptionalPropertyTypes, source maps and declaration files. rootDir and outDir are present but commented out.
Why does tsc file.ts fail with error TS5112?
Since TypeScript 6, passing file names to tsc in a folder that has a tsconfig.json is an error, because the files would be compiled without that config. Run plain tsc to use the config, or add --ignoreConfig to compile just the named files with the default options.