Course outline

TypeScript / Advanced / Lesson 17

JavaScript migration and toolchain integration

Adopt TypeScript incrementally while preserving known behavior, expose untyped boundaries, and keep transformation separate from semantic checking.

About 5 min readContent reviewed
On this page

Lesson goals

  • Plan migration around tested module boundaries.
  • Use allowJs, checkJs, and declarations with explicit assumptions.
  • Verify emitted runtime semantics after toolchain changes.

Preserve behavior before increasing precision

Start by recording existing entry points, module formats, tests, runtime versions, and build commands. A file rename is not a complete migration plan, and a wall of any annotations can conceal rather than resolve uncertainty. Choose a bounded module with clear inputs and outputs, capture its current behavior, and add types around the contract. Classify diagnostics as actual bugs, missing declarations, dynamic assumptions, or deliberate legacy behavior. Some repairs should change behavior, but that decision needs an explicit requirement and tests. Do not claim a migration is safe solely because the new compiler emits JavaScript.

Move a tested pure operation behind a typed boundary

The first function represents a small migrated numeric core. Its readonly parameter prevents mutation through this view, and its explicit result documents the public outcome. The runtime still uses JavaScript addition, so this model assumes inputs already satisfy the chosen numeric domain. A legacy caller providing text requires validation or a documented normalization layer before the function. Avoid changing coercion policy accidentally while presenting the work as annotation-only. Capture representative valid and invalid legacy inputs so the team can decide which behavior to preserve and which defects to reject. Migration tests should focus on real observable behavior.

typescript A typed core with preserved small-input results
export {};
function total(values: readonly number[]): number {
  return values.reduce((sum, value) => sum + value, 0);
}
console.log(total([2, 4, 6]), total([]));
Expected output
12 0

The nonempty values total twelve, and the empty input totals zero. The annotation itself performs no input conversion.

Use mixed JavaScript projects deliberately

allowJs permits JavaScript files in a TypeScript program, while checkJs requests checking for those files. JSDoc can express useful contracts without immediately rewriting every source file. These mechanisms help staged adoption but do not guarantee that imported JavaScript behaves like its annotations. Manage source and output directories so emitted files are not rediscovered as new inputs. Decide declaration ownership for third-party packages rather than inventing broad ambient modules that accept every use. A temporary migration escape should be narrow, tracked, and backed by an actual runtime assumption that will be resolved. Unchecked islands remain trust boundaries for the typed code.

Make the legacy boundary honest before use

The second function accepts unknown to model an untyped caller and checks the specific runtime requirement before using a string method. This is stronger than changing a legacy parameter to string while leaving arbitrary JavaScript callers unchanged. The error policy is deliberate here; a result union could be preferable for expected validation failures. Do not assume converting with String(value) is a neutral repair, because it accepts many values and can invoke custom conversion behavior. Choose whether the application rejects, normalizes, or defaults, then test that choice. Static migration and input-policy migration should be reviewable as separate decisions.

typescript A real runtime contract for an untyped edge
export {};
function normalizeName(value: unknown): string {
  if (typeof value !== "string") { throw new Error("name must be text"); }
  return value.trim();
}
console.log(normalizeName(" Mira "));
try { normalizeName(7); } catch (error) {
  console.log(error instanceof Error ? error.message : "unexpected failure");
}
Expected output
Mira
name must be text

Text is trimmed, while the numeric input is rejected by executable logic rather than a type assertion.

Verify the checker and transformer independently

Some build tools and runtimes can remove TypeScript syntax with version-specific restrictions, but that is not automatically semantic type checking. Keep the intended tsc check in the workflow when another tool emits code. Not every TypeScript construct is erased syntax: enums, namespaces, and parameter properties can require runtime transformation. Class fields, decorators, module interop, and extension handling need explicit mode choices. This course runs compiler-emitted JavaScript under its stated baseline instead of assuming direct execution support. Verify source maps and production entry points after toolchain changes, and test a clean build so cached artifacts cannot conceal a broken integration.

Common errors and fixes

Migration introduces any across most interfaces.

Why it happens
Diagnostics were hidden instead of classifying real boundaries.
How to fix it
Use unknown and focused contracts where evidence is missing.
Prevent it next time
Track and reduce temporary untyped escape hatches.

A transformer succeeds while semantic errors remain.

Why it happens
Syntax removal was mistaken for type checking.
How to fix it
Run the pinned semantic checker as a separate step.
Prevent it next time
Document which tool owns each build stage.

A migration silently changes numeric coercion.

Why it happens
Annotation work was combined with undocumented input normalization.
How to fix it
Specify and test the intended legacy acceptance policy.
Prevent it next time
Separate behavior changes from type-only changes.

Check your understanding

1. A legacy sum accepts ["2", "4"] through JavaScript coercion. Explain why annotating number[] is not a complete fix.

Show worked solution

The annotation constrains checked callers but does not alter an unchecked runtime array. Decide whether numeric strings should be rejected or parsed under a documented rule. Validate the boundary and add a runtime test for that exact legacy input. If behavior intentionally changes, describe the compatibility effect instead of presenting the annotation as automatic data repair.

2. A project uses a fast syntax transformer with isolatedModules. Which additional evidence is needed before claiming the TypeScript migration is verified?

Show worked solution

Run the full intended semantic check with the project compiler and options, then test the emitted entry points in the deployment runtime. isolatedModules enforces some single-file transformation restrictions but does not establish all cross-file type relationships. Verify module paths, declaration consumers, and runtime regression cases rather than relying only on successful transformation.

Key takeaways

  • Migrate boundaries with tests, not blanket any.
  • Keep data-policy changes explicit.
  • Transformation and semantic checking remain separate stages.

Official references