<< Home

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.

💡 You can measure progress by watching how often you need casts. Fewer casts usually means stricter configurations are working in your favor.

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;
}
⚠️ Treat community definitions as part of your dependency tree. They can break during upgrades; lock their versions like you lock runtime dependencies.

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.

💡 Use @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.

⚠️ Divergent file extensions or mismatched emit targets can cause runtime errors. Keep both environments aligned to avoid surprises.

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.