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.
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.
export {};
function total(values: readonly number[]): number {
return values.reduce((sum, value) => sum + value, 0);
}
console.log(total([2, 4, 6]), total([]));
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.
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");
}
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.