Skip to content
Chapter 3Lesson 8

Run TypeScript locally

Pin your project's Node version with mise, then run TypeScript files with the right tool for the job: native node, tsx, or tsc.

A new contributor clones the repo, types node seed.ts, and gets Unknown file extension ".ts". Their machine runs Node 22, but the project assumes Node 24, where running .ts files directly became built in. A README sentence asking everyone to upgrade won’t hold; a file the repo commits will. The same version gap runs through CI and production, where it surfaces weeks later as the bug that “works on my machine.”

This lesson closes that gap and answers the question that comes up the first time you run a .ts file: how does it actually execute? You’ll pin Node 24 in the repo, then run one .ts file three ways: native node, tsx, and tsc. By the end you’ll have a decision tree for choosing among them.

The runtime your code expects belongs to the repo, not the machine, just like the editor settings from the previous lesson. The committed file is .mise.toml, and what it pins is Node’s version.

The tool is mise (formerly rtx). It’s written in Rust, so it starts in milliseconds where nvm adds a second to every shell prompt, and it’s polyglot, so one tool pins Python, Go, and other runtimes alongside Node. This course uses mise from here on.

The pinned version is Node 24, which entered LTS in October 2025 and is supported through April 2028. LTS costs you nothing here: native TypeScript stripping (the next section’s topic) and this chapter’s ES2025 features all ship in Node 24.

The install is four steps. Step two, shell activation, is the one new users skip, which is why a fresh install can appear to do nothing.

  1. Install mise. One command per platform:

    • macOS: brew install mise
    • Linux: curl https://mise.run | sh
    • Windows: install WSL2 first, then follow the Linux path. The course doesn’t run on native Windows shells.
  2. Activate mise in your shell. Skip this and mise install runs, but node still resolves to whatever was on your PATH before. Add the activation line to your shell’s rc file:

    ~/.zshrc
    eval "$(mise activate zsh)"

    On bash, swap zsh for bash in both places. Then reload the shell: reopen the terminal, or run exec zsh (or exec bash) in the current one.

  3. Pin Node 24 at the repo root. From inside the project directory:

    Terminal window
    mise use --pin node@24

    The --pin flag writes the full version string (major plus latest patch) rather than the loose 24, so two developers running mise install a week apart get identical Node binaries. The command writes .mise.toml in the current directory:

    .mise.toml
    [tools]
    node = "24.4.1"

    Your patch number will differ depending on when you run the command. Commit this file: it’s how every other developer, CI, and production gets your runtime.

  4. Install the pinned version and verify. mise downloads Node into its own store, separate from any system Node:

    Terminal window
    mise install
    mise current

    mise current prints the active version for the current directory, such as node 24.4.1. From now on a terminal in this folder gives you Node 24 automatically, and one elsewhere leaves your other projects untouched.

Three tools run a .ts file. Native node is the default; the other two earn their place once a file needs something the default can’t do.

%%{init: {'themeCSS': '.node.term .nodeLabel { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }'} }%%
flowchart LR
  start([Run a .ts file])
  q1{"Needs path aliases,<br/>JSX, decorators,<br/>enums, or namespaces?"}
  q2{"Publishing<br/>a library?"}
  q3{"Type-checking<br/>only?"}

  tsx["<b>tsx</b><br/>dev only"]
  tscEmit["<b>tsc</b><br/>emits .js"]
  tscCheck["<b>tsc --noEmit</b>"]
  node["<b>node file.ts</b>"]

  start --> q1
  q1 -- Yes --> tsx
  q1 -- No --> q2
  q2 -- Yes --> tscEmit
  q2 -- No --> q3
  q3 -- Yes --> tscCheck
  q3 -- No --> node

  class tsx,tscEmit,tscCheck,node term
  classDef term fill:#1f2937,stroke:#94a3b8,color:#f8fafc
Three paths, one trigger per branch.

Node 24 reads .ts files natively. Its type-stripper blanks out the type-only parts of the source, such as annotations, interfaces, and type aliases, then runs the resulting JavaScript. No flag, no plugin, no build step.

What the stripper doesn’t do is why the other two paths exist:

  • It doesn’t read tsconfig.json. A path alias like @/lib/greet won’t resolve. Imports must be relative paths (./lib/greet.ts) or bare specifiers from node_modules.
  • It doesn’t transform. JSX, decorators, enum, and namespace compile to new runtime code, so they can’t be erased like a type annotation, and native Node rejects them.
  • It doesn’t type-check. A file with type errors still runs; verifying types is tsc --noEmit’s job, two subsections down.

So native node fits plain .ts files with relative imports and no code-generation syntax: seed scripts, one-off CLIs, throwaway calculations, anything you’d otherwise write as .js.

Terminal window
node hello.ts

tsx is a third-party CLI that runs .ts files like node, but reads tsconfig.json and uses esbuild to transform what native Node can’t. Reach for it when any one of five conditions holds:

  • Imports use path aliases from tsconfig.json (@/lib/..., ~/components/...).
  • The file contains JSX, like a standalone React component outside Next.js.
  • Decorators, rare in 2026 SaaS code but still seen in legacy frameworks.
  • enum, which this course never writes (as const objects and string-literal unions cover every case) but you’ll meet in older codebases.
  • namespace, same story: recognize it, don’t write it.

Installation comes in a later chapter, alongside pnpm and package.json. For now, run the one-shot form:

Terminal window
pnpm dlx tsx hello.ts

pnpm dlx downloads tsx, runs it once, and forgets it, the same pattern as npx. Once tsx is installed as a devDependency, this shortens to pnpm tsx hello.ts.

You’ll also meet ts-node in older docs; it has been deprioritized, so reach for tsx instead.

tsc: for type-checking and library publish

Section titled “tsc: for type-checking and library publish”

tsc, the TypeScript compiler, is the only one of the three that doesn’t run your code. It does two other things, each with its own trigger.

The first is type-checking the codebase. tsc --noEmit runs the full type-checker across every file tsconfig.json includes and emits no .js, the same check your editor runs in the background. From the project chapters onward, CI gates every PR on it, so a type error blocks the merge.

Terminal window
pnpm dlx tsc --noEmit

(pnpm dlx tsc is one-shot with no install, like pnpm dlx tsx. Once a package.json exists, the form is pnpm tsc --noEmit.)

The second is publishing a library to npm. tsc without --noEmit type-checks your .ts source and writes out the matching .js plus .d.ts type declarations, which is what publishable packages distribute. This course builds an application, not a library, so the emit path is named here only so you recognize it.

Neither trigger runs a .ts file: to execute code, reach for node or tsx; to verify types, reach for tsc.

Run all three tools on one file: node works, a path alias breaks it, tsx fixes it, tsc --noEmit type-checks it.

  1. Create hello.ts at the repo root. A typed greet and one console.log:

    hello.ts
    const greet = (name: string): string => `Hello, ${name}`;
    console.log(greet('world'));
  2. Run it with native node. Inside the same directory:

    Terminal window
    node hello.ts

    Output:

    Hello, world

    Node stripped the types and ran the JavaScript, no flag needed.

  3. Add a path alias. Move greet into src/lib/greet.ts and import it from hello.ts through the @/... alias. The new file:

    src/lib/greet.ts
    export const greet = (name: string): string => `Hello, ${name}`;

    And the updated hello.ts:

    hello.ts
    import { greet } from '@/lib/greet';
    console.log(greet('world'));

    The layout:

    • hello.ts imports from @/lib/greet
    • Directorysrc/
      • Directorylib/
        • greet.ts
  4. Re-run with native node and watch it fail. Same command as before:

    Terminal window
    node hello.ts

    Output (trimmed):

    node:internal/modules/package_json_reader:316
    throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
    ^
    Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@/lib' imported from /path/to/hello.ts

    Native Node doesn’t read tsconfig.json, so it reads @/lib as a node_modules package and throws when it can’t find one; the fix is a different tool, not relative imports.

  5. Add a minimal tsconfig.json and switch to tsx. Declare the alias in a minimal tsconfig.json at the repo root:

    tsconfig.json
    {
    "compilerOptions": {
    "module": "preserve",
    "moduleResolution": "bundler",
    "baseUrl": ".",
    "paths": {
    "@/*": ["src/*"]
    }
    }
    }

    moduleResolution: "bundler" is the right fit for tsx; the full config comes later. Now run with tsx:

    Terminal window
    pnpm dlx tsx hello.ts

    Output:

    Hello, world

    Same source and output as step 2; tsx just resolved the alias.

  6. Type-check with tsc —noEmit. No execution, just the type-checker:

    Terminal window
    pnpm dlx tsc --noEmit

    Output:

    Nothing: tsc speaks up only on an error. To check it works, call greet(42) and rerun; you’ll get Argument of type 'number' is not assignable to parameter of type 'string' with the file and line. Change it back to make tsc --noEmit silent again.

Some answers repeat, because more than one trigger routes to the same tool.

Match each scenario to the execution path you'd reach for. Check the trigger first, then pick the tool. Click an item on the left, then its match on the right. Press Check when done.

A one-off seed script with plain relative imports and no JSX
node script.ts
A standalone CLI tool that imports utilities via @/lib/...
pnpm tsx script.ts
CI step that gates every PR on the type-checker passing
pnpm tsc --noEmit
A throwaway React component example outside a Next.js project
pnpm tsx script.ts (JSX trigger)
A library you’re publishing to npm
pnpm tsc (emits .js)

The official sources for the three tools you installed, plus the Node release schedule.