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.
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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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
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.
Frequently Asked Questions
Should I use tsc or tsx in my npm scripts?
Can I run multiple npm scripts at once?
How do I pass flags to tsc in an npm script?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.