Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Getting TypeScript Projects to Compile Cleanly

Globomantics runs a small monorepo: a shared 'core' package, an 'ingest' package that still leans on a legacy JavaScript parser, and a 'dashboard' app built with Vite. The team's builds have grown unpredictable: declaration files land in the wrong place, tsc and Vite disagree about which files matter, watch mode thrashes on CI, a strictness upgrade exposed unsafe assumptions at the ingest package's legacy JavaScript boundary, and nobody is sure which packages actually need rebuilding after a change. You are the engineer asked to write the orchestration logic that makes these decisions explicit and testable.

Lab platform
Lab Info
Level
Intermediate
Last updated
Nov 03, 2026
Duration
1h 0m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Step 1: Meet the Globomantics build orchestrator

    The Globomantics monorepo

    Globomantics ships a small TypeScript monorepo with three packages:

    • core: shared types and utilities every other package imports.
    • ingest: data pipeline code that still calls into a legacy, untyped JavaScript parser.
    • dashboard: a Vite-built app that consumes both core and ingest.

    The build has become unpredictable enough that nobody trusts a green CI run. Declaration files sometimes land next to the wrong outDir, tsc and Vite silently disagree about which files were actually checked versus bundled, watch mode burns CPU on CI runners, a recent strict upgrade exposed unsafe assumptions at ingest's legacy JavaScript boundary, and a core change sometimes fails to trigger a dashboard rebuild. ### What you're building

    You are not editing tsconfig.json files by hand in this lab. Instead, you will write the orchestration logic a build tool would run before, during, and after invoking tsc and Vite. Across Steps 2 to 5 you will implement five pieces of that logic as plain TypeScript modules with fully testable behavior:

    1. Path mapping: where a source file's .js output and .d.ts declaration should land given rootDir/outDir, and whether a file even belongs in the build.
    2. Type-check / bundle reconciliation: comparing what tsc type-checked against what Vite's manifest actually bundled.
    3. Watch strategy and config merging: choosing polling vs native file events, and merging a shared base config into a project's own without losing overrides.
    4. Legacy JS interop under strict mode: repairing the null-safety gaps that ingest's legacy parser exposes once strict checks apply.
    5. Project references and incremental rebuilds: using a reference graph and changed-file hashes to decide which packages actually need rebuilding.

    Each module lives under src/ and is exercised directly by tests, so you'll see the exact effect of every change without waiting on a real tsc or Vite invocation. ### How the steps build on each other

    Step 2 starts with the most mechanical piece, path mapping, because every later module assumes you already know how a source file maps to its build output. Step 3 layers the type-check/bundle reconciliation on top. Step 4 tackles watch mode and configuration merging, which depend on knowing how included/excluded files are determined. Step 5 closes with the two hardest problems: repairing the null-safety gaps in code that consumes legacy JavaScript output, and computing rebuild sets across project references. By the end you will have a small but complete orchestration library, each piece independently testable. ### Checking your progress as you work

    Each task is graded as soon as you complete it, and you can run the same checks yourself at any time in the Terminal. To run one task's test:

    bash runTest.sh "task 2.1"
    

    To run the whole suite:

    npx --no-install vitest run --reporter=dot
    

    Each failure names the task it belongs to, so the output tells you which task to go back to rather than which line to stare at.

    This project has a strict tsconfig.json, and each task's test runs the TypeScript compiler before it checks behavior, so a type error fails the task with the compiler's own message. To check your types on their own, run:

    npm run typecheck
    

    When the command returns to the prompt with no errors listed, your types are clean.

  2. Challenge

    Step 2: Map the source tree to its build outputs

    Mapping source files to their outputs

    When tsc compiles with rootDir and outDir set, it re-parents the source tree: a file at rootDir/widgets/parser.ts lands at outDir/widgets/parser.js, preserving every intermediate directory. Get this mapping wrong in your own tooling and you'll look for build artifacts in the wrong place, or clobber files across packages that happen to share a directory name. src/pathMapper.ts already has the shared path-normalizing helpers the team relies on elsewhere; you're about to add the first mapping function that uses them. ### Declaration files follow the same shape, with two conditions

    .d.ts files land in the same relative slot as compiled .js output, but the extension swap is different, and TSX source needs its trailing x stripped before .d.ts is appended (Button.tsx emits Button.d.ts, never Button.d.tsx). Two compiler options also decide whether and where they land. tsc emits declaration files only when declaration is true, so with it off there is no path to report at all. And when declarationDir is set, declarations go under it instead of outDir, while the compiled .js files stay in outDir. Overlooking either one is how a package ends up looking for its types in the wrong place, or in a place that was never written. ### Not every file under rootDir belongs in the build

    include/exclude patterns in a tsconfig.json decide which files under rootDir are actually part of the project. A stray .spec.ts file or a scratch file left in a package directory can otherwise get mapped to an output path nobody expects. Before the orchestrator trusts a source tree, it needs to partition files into those the include patterns cover and those they don't.

  3. Challenge

    Step 3: Reconcile the type-checker with the bundler

    Diagnostics don't arrive pre-organized

    A no-emit type-check pass produces a flat stream of diagnostics, each tagged with a file, a severity, and a message, but nothing groups them by file or tells you which files are worst affected. src/diagnostics.ts holds the raw diagnostic shape the simulated checker emits; you'll turn that stream into per-file summaries the rest of the orchestrator can act on. ### Vite's manifest tells a different story

    Vite's production manifest records every module it actually bundled, including transitively-imported files pulled in through an entry point's imports list. src/manifest.ts defines the manifest entry shape; you'll flatten that structure into the plain list of bundled file paths the reconciler needs to compare against what tsc checked. ### Where tsc and Vite disagree

    A file can be type-checked but never bundled (dead code the checker still walks), or bundled but never type-checked (a .js file Vite happily pulls in that tsc skipped). Both are bugs worth flagging. src/reconciler.ts is where the checker's file list and the bundled-file list from parseManifest finally meet.

    Take the checker's side from its own list of checked files, the way tsc --listFilesOnly prints it, and not from collectDiagnostics. A clean file has no diagnostics, so it never appears in those summaries, and it would be wrongly reported as bundled but not type-checked.

  4. Challenge

    Step 4: Pick a watch strategy and merge shared configuration

    Watch mode isn't one-size-fits-all

    Native filesystem events are cheap and instant, but CI containers frequently run on filesystems where those events don't fire reliably, and native watchers can also choke once a project has enough files. src/watchStrategy.ts holds the environment-signal shape the orchestrator already collects; you'll turn those signals into an actual polling-vs-native decision. ### Shared config, project overrides

    The Globomantics packages all extends a shared base tsconfig.json, and tsc resolves that in two different ways. compilerOptions merge key by key, one level deep: a key the project sets replaces the base's value as a whole, so a project's lib or paths replaces the base's instead of combining with it. include, exclude and files do not merge at all: when the project defines one of them, its list replaces the base's, and when it leaves one out, it inherits the base's.

    One more rule from real extends is left out here: relative paths in a base config resolve against the base file's own directory, not the project's. The simplified shape in src/configMerger.ts has no file locations, so you will implement just the merge rules above. ### From merged config to watch settings

    Once a project's effective include and exclude are known, the watcher needs concrete input: the globs to subscribe to, and the patterns to ignore. They do different jobs. include picks the files to watch, and exclude filters files out afterwards, so it belongs in the watcher's ignore option and does not edit the include strings. src/watchGlobs.ts is where the merged configuration from the previous task becomes those two lists.

  5. Challenge

    Step 5: Tighten checks on legacy JS and wire up project references

    Strict mode and the legacy boundary

    strict is a bundle of compiler flags, not a single check. The one that matters here is strictNullChecks, which makes null and undefined types of their own, so a value typed string | null cannot be used as a string until you handle the null case. The compiler can only do that for values it has types for. A value typed any, which is what a call into untyped JavaScript can give you, opts out of those checks, and a bad value flows straight through.

    ingest calls a legacy JavaScript parser whose records can arrive with a missing sku, a null tags list, or a quantity that is a numeric string. src/ingestParser.ts describes that output with the LegacyRawRecord type and converts it into a strictly typed ParsedRecord. It builds as it stands, because the stub ignores its input and returns a fixed value, but it does not convert anything yet. The types tell you which cases to handle, and turning each one into a safe value is runtime code you write. ### Project references form a dependency graph

    dashboard references ingest, which references core, a directed acyclic graph, at least when nobody's made a mistake. src/projectGraph.ts holds the package descriptors with their direct reference lists; you'll compute the full transitive closure each package depends on, and catch the cycle that would otherwise make an incremental build loop forever. ### Deciding what actually needs rebuilding

    A changed file only matters if its content hash actually differs from last build, and once a package is known to have changed, every package that transitively depends on it, via the graph you just built, needs rebuilding too, even if none of its own files changed. src/rebuildPlanner.ts ties changed-file hashes to the reference graph to produce that final rebuild set.

  6. Challenge

    Step 6: Run and check the application

    Every task is done, and each one was graded the moment you completed it. What you have not done yet is watch the whole application run at once, which is the only place the pieces you wrote separately become a single working program.

    Start the application

    Open the Terminal tab and start the dev server:

    npm run dev
    

    It serves the application on port 5173. Switch to the Web Browser tab to use it: the page is running the functions you implemented in the previous steps, so what you see is your own code rather than a demonstration of it.

    The page has one section per module you wrote: path mapping, grouped diagnostics, manifest coverage, the watch strategy and globs, a legacy ingest record, and the project graph with its rebuild plan. Each section started empty or zeroed and now shows real values. The Packages to rebuild line lists the changed package plus every package that depends on it.

    The server reloads on save, so you can change a file and see the result without restarting anything. When you are done looking around, press Ctrl+C in the Terminal to stop the server and free the terminal for the checks below. ### Run the final checks

    The checks you used along the way work the same now. With all twelve tasks done, run the whole suite and the TypeScript compiler on your finished code in the Terminal:

    npx --no-install vitest run --reporter=dot
    npm run typecheck
    

    The suite reports every test passing. After the two lines npm prints for the script, typecheck returns to the prompt with no errors listed, which means strict mode found no type errors in src/ or tests/. Two things are worth trying before you finish. First, break something on purpose and watch the matching check fail, then put it back. Knowing what a failure looks like is worth as much as knowing what a pass looks like, and it costs you nothing here.

    Second, ask the compiler to catch a type mistake. In src/pathMapper.ts, add return 42; as the first line of resolveOutputPath's body, then run the compiler again:

    npm run typecheck
    

    The compiler reports that type number is not assignable to type string, and bash runTest.sh "task 2.1" fails on that error before it checks any output. Remove the line when you are done. Congratulations on completing the Globomantics build orchestrator.

    You built the orchestration logic behind it: path mapping from source files to compiled and declaration outputs, reconciliation of type-check diagnostics with a bundler manifest, watch strategy and shared-config merging, repairs for null-safety gaps in legacy JavaScript interop, and a project reference graph that decides which packages an incremental build must rebuild. The page you ran in the browser calls every one of those modules.

    You also ran TypeScript's strict compiler over all of it and watched it catch a type mistake before any test did. Reasoning about where build output lands, what the type-checker and bundler each see, and what a change really invalidates are skills that carry over to any multi-package TypeScript project.

About the author

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight