Use npm Scripts with TypeScript

Organize your TypeScript workflow with npm scripts for building, type checking, linting, testing, and running dev servers from a single package.json.

5 min read

npm scripts are commands defined in your package.json that you run with npm run followed by the script name. They replace ad-hoc terminal commands with a standardized interface that every contributor on your team can use. For a TypeScript project, a well-designed set of npm scripts covers building, type checking, linting, testing, and running development servers.

Instead of remembering whether to run tsc --noEmit or npx eslint or npx vitest, your team runs npm run build, npm run lint, and npm run test. The scripts become the single entry point to every tool in your project.

The Core TypeScript Scripts

Every TypeScript project needs at least three scripts: one to type-check, one to build, and one to watch for changes during development. Here is a minimal set:

jsonjson
{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "tsc",
    "dev": "tsc --watch"
  }
}

The typecheck script runs the compiler without producing JavaScript output. It is fast and ideal for CI pipelines where you only need to verify correctness, not emit files. The build script compiles your project and writes .js files to the output directory. The dev script watches your source files and recompiles on every save.

For projects that use a fast runtime like tsx during development, the dev script changes. tsc --watch is replaced with tsx --watch, and type checking moves to a separate step. See tsx vs ts-node for Running TypeScript for choosing the right runner.

Add Linting and Formatting

A complete TypeScript project also needs linting and formatting. Add scripts for both:

jsonjson
{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "tsc",
    "dev": "tsc --watch",
    "lint": "eslint .",
    "format": "prettier . --write",
    "format:check": "prettier . --check"
  }
}

The lint script runs ESLint across the project. The format script fixes formatting in place. The format:check variant verifies formatting without modifying files, which is what you run in CI to block unformatted code from merging.

For the full ESLint setup, see Set Up ESLint for TypeScript. For the full Prettier setup, including how to keep it from fighting with ESLint over formatting rules, see Set Up Prettier for TypeScript.

Add Testing

Testing scripts depend on your test runner. If you use Vitest, add these:

jsonjson
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:typecheck": "vitest --typecheck"
  }
}

The test script starts Vitest in watch mode for development. The test:run variant runs tests once and exits, for CI. The test:typecheck variant runs compile-time type assertions without executing any code.

Chain Commands for CI

CI pipelines need to run multiple checks in sequence and fail fast if any step fails. Chain commands with the && operator:

jsonjson
{
  "scripts": {
    "ci": "npm run typecheck && npm run lint && npm run test:run && npm run format:check"
  }
}

If typecheck fails, none of the later steps run. This is exactly what you want in CI: stop at the first failure to save time. Each command produces clear output so you can tell which step failed from the CI logs.

Use Pre and Post Hooks

npm supports pre and post hooks that run automatically before and after a script. If you define a prebuild script, npm runs it before build every time:

jsonjson
{
  "scripts": {
    "prebuild": "npm run typecheck",
    "build": "tsc",
    "postbuild": "echo Build complete"
  }
}

Now running npm run build first type-checks, then compiles, then prints a confirmation.

The hooks are implicit, so any contributor running build gets the full pipeline without needing to remember the extra steps.

Organize Scripts by Concern

As your project grows, group scripts by what they do. Use colon-separated names to show relationships:

jsonjson
{
  "scripts": {
    "check:types": "tsc --noEmit",
    "check:lint": "eslint .",
    "check:format": "prettier . --check",
    "check:all": "npm run check:types && npm run check:lint && npm run check:format",
    "build:prod": "tsc --project tsconfig.prod.json",
    "dev:server": "tsx --watch src/server.ts",
    "dev:worker": "tsx --watch src/worker.ts"
  }
}

The check: prefix groups all verification scripts. The build: prefix supports multiple build targets. The dev: prefix separates different development processes.

Keep Scripts Cross-Platform

npm scripts run in the system shell. On macOS and Linux that is usually bash. On Windows it is cmd or PowerShell.

To keep scripts portable, avoid shell-specific syntax like environment variable assignment or complex pipes. Stick to the commands the tools themselves provide.

For anything complex, extract it into a Node.js script or a dedicated tool instead of embedding it in the shell command. A script that starts as a one-liner often grows, and shell syntax is harder to maintain than TypeScript.

Rune AI

Rune AI

Key Insights

  • Define build, dev, lint, test, and typecheck scripts in package.json for a complete TypeScript workflow.
  • Use tsc --noEmit for fast type checking without producing JavaScript output.
  • Chain commands with && for sequential CI pipelines like lint, typecheck, and test.
  • Use tsx for dev scripts where startup speed matters more than type checking.
  • Keep script names short and consistent across projects so every teammate knows them.
RunePowered by Rune AI

Frequently Asked Questions

Should I use tsc or tsx in my npm scripts?

Use tsc for production builds where you need type checking and JavaScript output. Use tsx for dev scripts where startup speed matters. A common pattern is tsc --noEmit for type checking and tsx for running TypeScript during development.

Can I run multiple npm scripts at once?

Yes. Chain them with && for sequential execution. For parallel execution, use the concurrently package or the & operator. Sequential is safer for build pipelines where order matters.

How do I pass flags to tsc in an npm script?

Add them directly after the command. For example, tsc --noEmit --watch. Use -- before flags if you need to pass flags through npm to the underlying command.

Conclusion

npm scripts turn your TypeScript toolchain into a set of memorable commands. Use tsc --noEmit for type checking, tsc for builds, tsx for running scripts fast, and chain commands for CI pipelines. Every contributor runs the same commands and gets the same results.