Type Checking in CI

Type Checking in CI and Building for Production

Because nothing in the run path checks types, something else has to. tsc --noEmit reads the config from tsconfig.json, reports every error, and exits non-zero if there are any.

Output of 111
$ npx tsc --noEmit
src/bad.ts(2,21): error TS2322: Type 'string' is not assignable to type 'number'.
src/bad.ts(3,24): error TS2551: Property 'toFixed' does not exist on type 'string'.
Did you mean 'fixed'?
$ echo $?
1

Run that same file under Node and it starts happily, gets through the assignment, and dies at the method call with TypeError: greet(...).toFixed is not a function — one error instead of two, found later.

package.json — the four scripts a Node TypeScript service needsJSON
"scripts": {
  "dev": "node --watch --env-file=.env src/server.ts",
  "typecheck": "tsc --noEmit",
  "build": "tsc -p tsconfig.build.json",
  "start": "node --enable-source-maps dist/server.js"
}

node --run typecheck (Watch Mode and Scripts) invokes these without npm 2,036 's process overhead. In CI a job that runs npm ci, npm run typecheck and node --test is enough, and on TypeScript 7 it is fast enough to run on every branch.

tsconfig.build.json extends the checking config and turns emit on: "noEmit": false, "outDir": "dist", "rootDir": "src" and "sourceMap": true. rewriteRelativeImportExtensions then does the one thing that makes this workflow hold together:

dist/server.js — emitted from src/server.tsJavaScript
import { createServer } from 'node:http';
import { greet } from './greet.js';
const port = Number(process.env.PORT ?? 3000);
const ada = { id: 1, name: 'Ada' };
createServer((_req, res) => res.end(greet(ada))).listen(port, () => {
    console.log(`listening on ${port}`);
});
//# sourceMappingURL=server.js.map

The ./greet.ts specifier became ./greet.js, so the same source ran unbuilt in development and runs built in production; --enable-source-maps in the start script maps stack traces back to the .ts lines.

Is the build step even necessary? You can copy src into the image and run node src/server.ts instead. What you give up is the guarantee that what you deploy is what you checked: a stray enum or a missing extension then fails at request time rather than at build time. Ship dist for anything a customer touches; run .ts directly for scripts and internal tools.