Chapter 7: Working with the JavaScript Ecosystem
TypeScript sits inside the wider JavaScript world like a lens that sharpens everything passing through it. You keep writing regular JavaScript; TypeScript adds structure and safety without changing the underlying runtime. This chapter explores how to adopt stricter rules gradually, how to work with third party libraries that may or may not have types, how JSDoc can add structure to plain JavaScript, and how to blend both languages in projects that grow organically.
Strict mode and incremental adoption
Strict mode in TypeScript is a bundle of checks that tighten your code. It does not change runtime behavior; the effect is entirely in the compiler. Teams often start with partial strictness then gradually enable the full set as the project matures. This approach keeps friction low while guiding the codebase toward predictable behavior.
| Option | Description |
strictNullChecks |
Ensures null or undefined are handled explicitly |
noImplicitAny |
Requires every value to have a known type |
strictFunctionTypes |
Makes function assignments safer by checking parameter variance |
alwaysStrict |
Emits JavaScript in strict mode and parses modules accordingly |
The safest path is to switch on strict then override individual items only if the migration becomes difficult. Keep exceptions temporary; revisit them as code stabilizes.
Gradual rollout inside large repositories
For monorepos or multi package setups, you can adopt strict mode package by package. Turn on strict in leaf packages first; this limits the blast radius while helping you strengthen the foundation.
// packages/widget/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"strict": true
}
}
After finishing the leaf packages, move upward until every package compiles with the same set of strict rules.
Typing third party libraries
Many JavaScript libraries ship with bundled types; many others rely on community maintained definitions. TypeScript can only offer strong checks when it knows the shapes it is working with, so understanding where types come from is essential.
| Source | How types are delivered |
| Bundled with the library | package.json includes a types entry |
| Community maintained | Install from @types/… on DefinitelyTyped |
| Absent entirely | Create a .d.ts stub with partial shapes |
When a library does not ship types, you can install a matching definition from DefinitelyTyped. These packages follow the library version through a convention where the major version stays aligned. If no suitable definition exists, write your own ambient module.
// minimal types for an untyped package
declare module "cool-js-lib" {
export function init(id: string): void;
export function shutdown(): void;
}
Contributing upstream
If you discover missing or incorrect definitions, you can improve the ecosystem by contributing fixes. For libraries you control, add .d.ts or migrate to TypeScript. For libraries maintained by others, contribute to DefinitelyTyped. Clear versioning and small, focused changes keep these contributions easy to review.
JSDoc types and JS projects
JavaScript projects can benefit from TypeScript checking without converting to .ts files. JSDoc annotations let you describe shapes inside plain JavaScript. With checkJS enabled, the compiler treats JSDoc comments as type hints and reports mismatches in the same way it reports them for TypeScript.
Annotating functions
JSDoc tags give JavaScript a thin layer of structure. They are lightweight, readable, and friendly to mixed teams where not everyone works directly with TypeScript.
// utils.js
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
export function add(a, b) {
return a + b;
}
When checkJS is active, TypeScript validates calls to add and warns if arguments or return values do not match the annotated shapes.
@typedef or @template to create reusable structures or generics that span several functions.
Mixed codebases
Real world projects often mix .js and .ts files. TypeScript handles these setups smoothly. allowJs lets the compiler include JavaScript files; checkJs turns on full semantic checking for them.
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"outDir": "dist"
},
"include": ["src"]
}
You can migrate files one by one from .js to .ts; TypeScript uses JSDoc types until you are ready to switch to full annotations.
Sharing types between TS and JS
Shared type definitions help both file kinds understand a common domain. Place reusable types in .d.ts files and let both JavaScript and TypeScript import them or receive them through global augmentation.
// types/domain.d.ts
export interface UserProfile {
id: string;
email: string;
}
JavaScript consumers can import these types purely for documentation or use @typedef to leverage them indirectly.
Interoperating with plain JavaScript
Most teams adopt TypeScript gradually; the language is designed to fit alongside existing JavaScript rather than replace it in one step. Interoperation works well when boundaries are clear and when you plan for a little flexibility at the edges.
Calling JS from TS
You can call JavaScript files directly from TypeScript. The compiler infers types when possible then falls back to any if the shapes are unclear. Adding JSDoc or ambient types strengthens the connection without rewriting the file.
// js/logger.js
export function log(msg) {
console.log("[log]", msg);
}
// ts/app.ts
import { log } from "./logger.js";
log("ready");
The import resolves normally. The only difference is that TypeScript works with inferred shapes rather than declared ones.
Calling TS from JS
JavaScript consumers can import TypeScript compiled output like any other module. Ship the JavaScript files and keep the .d.ts definitions alongside them so editors supply IntelliSense. From the JavaScript side, the code behaves like a normal library.
// js/main.js
import { sum } from "../dist/math.js";
console.log(sum(3, 4));
This flow preserves a clean runtime story. TypeScript remains an authoring tool while JavaScript continues to run unmodified.
Soft edges between both worlds
As projects evolve, the boundary between JavaScript and TypeScript shifts. Some teams keep utility files in JavaScript for fast sketching; others keep legacy directories untouched until time allows for migration. Both approaches work when you maintain clear interfaces and publish accurate types. The goal is to let both languages cooperate without friction.