---
title: 'Build and Run Flow'
source: 'https://academia.sh/en/courses/typescript/build-and-run-flow'
course: TypeScript
language: en
updated: '2026-08-17T18:09:55+00:00'
license: 'CC BY-SA 4.0'
---

# Build and Run Flow

Separating type checking from publish output, publish behavior on error, the options enabling per-file transpilation, type-only imports, and type-stripping runtimes.

Throughout the course, the compiler both did type checking and produced output.
Separating these two jobs is a decision that determines a project's build time and
deployment layout.

The reason for the separation was established in the first lesson: types are erased
at compile time. Erasure is mechanical and fast; type checking, on the other hand,
requires resolving the entire program and is slow. Splitting the two jobs shortens the
development loop.

## Publishing on Error

The behavior seen in the first lesson is re-examined here, this time in a project
context. `src/main.ts`:

```typescript
const value: number = "21.4";
console.log(value);
```

If the configuration is set to produce output with `outDir`, compilation gives this
diagnostic:

```text
src/main.ts(1,7): error TS2322: Type 'string' is not assignable to type 'number'.
```

and still writes the `dist/main.js` file:

```javascript
const value = "21.4";
console.log(value);
export {};
```

The `noEmitOnError` option changes this behavior; the same compilation gives the
diagnostic and the `dist` directory is never created.

Which is wanted depends on context. During development, running code despite a type
error is useful — the error is in one module, the behavior being tried is in another.
In the publish pipeline, the opposite holds: code that has not passed checking should
not be packaged. Using two separate configurations is common for this reason.

The trailing `export {};` line is a separate detail: the compiler adds it to mark the
file as a module. The `moduleDetection` setting from the Module Resolution lesson
determines this behavior.

## Splitting Into Two Steps

The split is set up with two commands:

- **Type checking:** `tsc --noEmit`. Produces no output, only gives diagnostics.
- **Output production:** A transpiler that runs per file. Erases types, adapts syntax
  to the target, does not do type checking.

The reason the second step is fast is that it can run **per file**: transpiling one
file requires no looking at other files. But this is impossible for some TypeScript
notations — the transpilation needs type information to be correct.

The `isolatedModules` option catches these notations. The following
`src/measurement.ts`:

```typescript
export interface Measurement {
  id: string;
  value: number;
}

export function average(records: readonly Measurement[]): number {
  return records.length === 0
    ? 0
    : records.reduce((t, r) => t + r.value, 0) / records.length;
}
```

and `src/main.ts`:

```typescript
import { Measurement, average } from "./measurement.js";

export { Measurement };

const records: Measurement[] = [{ id: "s-01", value: 21.4 }];
console.log(average(records));
```

With `isolatedModules` on:

```text
src/main.ts(3,10): error TS1205: Re-exporting a type when 'isolatedModules' is enabled requires using 'export type'.
```

The problem is this: a transpiler running per file cannot know whether the name
`Measurement` is a type or a value. If it is a type, the export line has to be erased
entirely; if it is a value, it has to be kept. Deciding requires looking at the
`measurement.ts` file, and that breaks per-file transpilation.

The `verbatimModuleSyntax` option applies the same rule to imports too:

```text
src/main.ts(1,10): error TS1484: 'Measurement' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
src/main.ts(3,10): error TS1205: Re-exporting a type when 'verbatimModuleSyntax' is enabled requires using 'export type'.
```

The fix is using type-only notations:

```typescript
import type { Measurement } from "./measurement.js";
import { average } from "./measurement.js";

export type { Measurement };

const records: Measurement[] = [{ id: "s-01", value: 21.4 }];
console.log(average(records));
```

This file compiles without error. The produced `dist/main.js`:

```javascript
import { average } from "./measurement.js";
const records = [{ id: "s-01", value: 21.4 }];
console.log(average(records));
```

The output of `node dist/main.js` is `21.4`.

The `import type` notation says **in the declaration** that the line will be erased
entirely. The gain is not only speed: taking only a type from a module whose import
has a side effect does not cause that module to load in the output. Same principle as
tree shaking in the Modules, Tooling and the Ecosystem course — knowing an erasable
dependency is erasable.

## Incremental Builds

The `incremental` option makes the compiler write information left over from its
previous run to a file. On the next build, only changed files and the ones that
depend on them are re-resolved.

With the option on, a file named `tsconfig.tsbuildinfo` is created at the project
root. This file is a build artifact; it is not put under version control and is
removed in cleanup steps. Its location can be changed with `tsBuildInfoFile`.

Watch mode (`tsc --watch`) carries the same idea into a continuously running
process: file changes are watched, and only the affected part is re-checked. During
development, this mode is preferred over a one-shot build.

## Type-Stripping Runtimes

Some runtimes can run TypeScript files directly: they strip type annotations and
execute the remaining JavaScript. This is the first lesson's type erasure principle
carried over to runtime.

The distinction has to be seen. The following file contains a type error:

```typescript
const value: number = "21.4";
console.log(value);
```

In such a runtime, the file runs and prints `21.4`. **No type checking happens at
all.** Stripping is mechanical; it has nothing to do with the guarantee a compiler
gives.

The second limit shows up in TypeScript notations that produce code:

```typescript
enum Unit {
  Celsius = "C",
}
console.log(Unit.Celsius);
```

This file does not run:

```text
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
```

The Basic Types lesson showed that an enum is the exception to the type erasure
principle; its consequence is concrete here. A runtime that only strips cannot run
syntax that cannot be erased. The same restriction covers parameter properties too.

For this reason, the `erasableSyntaxOnly` option is kept on in projects targeting a
type-stripping runtime: notations that cannot be erased are reported at compile time,
instead of turning into a runtime surprise.

Comparison of the three paths:

| Path | Type checking | Speed | Constraint |
|---|---|---|---|
| Compiling with the compiler | Yes | Low | — |
| Separate checking + per-file transpilation | Separate step | High | Requires `isolatedModules` |
| Type-stripping runtime | No | High | Non-erasable syntax does not run |

What the three have in common is that type checking must not **disappear**. In the
second and third paths, checking has to happen somewhere — in the publish pipeline,
in the editor, in a pre-processing hook. When it does not happen, what remains is
JavaScript with type annotations written on it.

## Summary

- A type error does not by itself stop publishing; `noEmitOnError` changes this, and
  separate configurations for development and publishing are common.
- Type checking (`tsc --noEmit`) and output production can be split into separate
  steps; the second step's speed comes from being able to run per file.
- `isolatedModules` and `verbatimModuleSyntax` report notations that break per-file
  transpilation; the fix is `import type` and `export type` notation.
- The `incremental` option writes build information to a file; this file is a build
  artifact and is not put under version control.
- Type-stripping runtimes do no type checking at all and cannot run non-erasable
  syntax; `erasableSyntaxOnly` catches these notations at compile time.

## Course Wrap-Up

The course began with a single question: what can be said about a program without
running it? TypeScript's answer was a layer of proof built on source code that leaves
nothing behind once translation is done.

This layer's limits were drawn from the start and re-tested in every topic. Types are
erased at compile time; they do not change runtime behavior; they do not prove the
shape of the outside world. These constraints are not a shortcoming, they are the
definition of what the layer is — and the non-working output of the `paths` setting
and the lack of checking in a type-stripping runtime are two different views of the
same constraint.

A single model was carried through the whole course. The measurement record started
as a JavaScript object carrying no guarantees; its fields were typed, its unit was
bound to a closed set, its valid and invalid states were split apart with a
discriminated union, its source was abstracted with an interface, its log was
generalized with a type parameter, its derivatives were computed with mapped and
conditional types, its boundary was guarded with a type predicate, and finally it was
published outward with a package manifest. What was gained at every step was the
same thing: moving a problem that would surface at runtime to compile time.

Three principles to take from the course:

**Proof is produced at the boundary.** Data from a system boundary is received as
`unknown`, checked at runtime, and the result is recorded with a type predicate. The
`as` notation is not proof, it is a debt.

**The model stays in one place.** Derived types are not written by hand, they are
computed from the real type; fixed lists come from a single source, both as a value
and as a type. The model's growth forces, through the compiler, a review of the code
that uses it.

**Strictness is not a preference, it is a scope decision.** Every flag corresponds to
an error class; leaving a flag off means leaving that class to runtime.

Next comes where this code will run. The **Node.js Runtime** course covers the
server-side runtime: the phases of the event loop, filesystem and network interfaces,
streams and backpressure, process management, and logging and monitoring under
production conditions. The type layer built in this course holds there too — the
interfaces the runtime offers are typed with declaration files, and the module
resolution rules are that runtime's rules.
