TypeScript / THE COURSESearch
STAGE 04 / LIBRARIES & SCALE

LESSON 29 OF 36 · 24 MIN + PRACTICE

Workspaces & project boundaries

Organize packages and builds so dependencies stay clear as the repository grows.

EXAMPLES CHECKED WITH TYPESCRIPT 7.0.2 / NODE 24

Where you are

You have a library and an application consuming it. A workspace simplifies local coordination, but convenience can mask missing declarations or accidental source imports. Package boundaries must remain real release boundaries.

Mental model

The package manager owns installation and links. TypeScript project references describe build dependencies. Runtime imports load installed exports. CI schedules checks and releases. These graphs overlap but are not the same graph.

JavaScript and package reality

A workspace link can expose source files that will not exist in a published archive. Root dependencies can accidentally satisfy undeclared package dependencies. Test packed consumers outside the workspace to expose both mistakes. Imports should follow package ownership, not arbitrary relative paths into a sibling's src directory.

TypeScript model

Project references let a composite project depend on another project's declaration output. Build mode with tsc -b understands ordering and incremental state. A clean build and an incremental build exercise different behavior; both matter in CI and local development.

TypeScript 7 can parallelize checking within a project and project-reference builds across the graph. Its checkers and builders controls trade CPU and memory; multiplying both can oversubscribe a small CI machine. Do not preserve old workarounds without measuring the native compiler. References remain useful for ownership and dependency boundaries even when raw checking becomes faster.

Working example

Source: examples/valid/29.ts

TypeScript
interface JobRepository {
  find(id: string): Promise<string | undefined>;
}
const memory: JobRepository = {
  async find(id) {
    return id === "j1" ? "Import" : undefined;
  },
};
console.log(await memory.find("j1"));
export {};

Verified runtime output:

text
Import

The small repository interface is independent of filesystem details. The package lab shows the same principle across real package boundaries rather than only within one file.

Reference layout

Use a solution config containing files and references, with each referenced package owning its source, output, and composite setting:

json
{
  "files": [],
  "references": [{ "path": "./packages/domain" }, { "path": "./packages/application" }]
}

The application project's own references must also record its domain dependency. A solution's list alone is not the dependency edge. Build with npx tsc -b, inspect outputs, change the domain contract, and observe the required rebuild.

Type-checker drill

Intentionally invalid: examples/invalid/29.ts

TypeScript
interface Repository {
  find(id: string): Promise<string | undefined>;
}
const repository: Repository = {
  async find() {
    return 42;
  },
};
export {};

Actual TypeScript 7.0.2 diagnostic:

text
examples/invalid/29.ts(5,9): error TS2322: Type '() => Promise<number>' is not assignable to type '(id: string) => Promise<string | undefined>'.
  Type 'Promise<number>' is not assignable to type 'Promise<string | undefined>'.
    Type 'number' is not assignable to type 'string'.

The adapter returns a number where the domain contract promises text. Fix the adapter boundary, not the whole domain to accommodate one storage representation. This is the value of a small explicit interface across packages.

Runtime drill

Remove a dependency from one package's manifest while keeping it at the workspace root. See whether local imports still work. Pack and install the package in isolation to expose the missing declaration. Restore the dependency in its actual owner.

Professional pattern

Share policy only where environments agree. Keep Node globals out of browser packages and avoid one giant ambient type environment for the entire repository. Enforce dependency direction through imports and manifests. Cache builds using inputs that include compiler, lockfile, and config; an incomplete cache key can reuse incompatible declarations.

Exercise

Create domain, CLI, and browser-summary packages. Give each an independent config and explicit dependencies. Add references where declaration-based builds help. Verify a clean build, a no-change incremental build, and a domain-change rebuild. Measure before selecting parallelism settings. Test the domain tarball outside the workspace.

Checkpoint

  • You distinguish workspace links from published dependencies.
  • You can describe a project-reference edge.
  • You avoid globally shared environment types.
  • You measure native compiler behavior before tuning.

Sources

Project references, npm workspaces, and TypeScript 7 parallel builds.

Check off this lesson when you can meet its checkpoint. Progress stays in this browser.