Hands‑On Exercise: Migrate a Small JS Module to TypeScript¶
We’ll start with a small JavaScript module that processes user data.
It’s intentionally messy—just like real legacy code.
Step 1 — Start With a Plain JavaScript Module¶
userUtils.js¶
function normalizeUser(user) {
return {
id: user.id,
name: user.name.trim(),
email: user.email.toLowerCase()
}
}
function isValidEmail(email) {
return email.includes("@")
}
module.exports = {
normalizeUser,
isValidEmail
}
This module:
- has no types
- accepts anything
- can easily break at runtime
We’ll migrate it safely.
Step 2 — Enable JS Support in TypeScript¶
Install TypeScript:
npm install --save-dev typescript @types/node
Initialize:
npx tsc --init
Update tsconfig.json:
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"outDir": "dist"
},
"include": ["src"]
}
This allows TypeScript to compile JS files without type‑checking them yet.
Step 3 — Add JSDoc Types (No Renaming Yet)¶
Before converting to .ts, we add types using JSDoc.
This gives us type safety inside JavaScript.
Modify:
userUtils.js¶
/**
* @typedef {Object} User
* @property {string} id
* @property {string} name
* @property {string} email
*/
/**
* @param {User} user
* @returns {User}
*/
function normalizeUser(user) {
return {
id: user.id,
name: user.name.trim(),
email: user.email.toLowerCase()
}
}
/**
* @param {string} email
* @returns {boolean}
*/
function isValidEmail(email) {
return email.includes("@")
}
module.exports = {
normalizeUser,
isValidEmail
}
Now TypeScript understands:
normalizeUseraccepts and returns aUserisValidEmailaccepts a string- invalid usage will be flagged
This is a zero‑friction improvement.
Step 4 — Turn on checkJs to Type‑Check JS Files¶
Update tsconfig.json:
"checkJs": true
Now TypeScript will catch errors in .js files.
Example error caught¶
If someone calls:
normalizeUser(123)
TypeScript will warn:
Argument of type 'number' is not assignable to parameter of type 'User'.
We’ve improved safety without renaming anything.
Step 5 — Convert the File to TypeScript¶
Rename:
userUtils.js → userUtils.ts
The JSDoc types still work, but now we can replace them with real TypeScript types.
Step 6 — Replace JSDoc with TypeScript Types¶
userUtils.ts¶
export type User = {
id: string
name: string
email: string
}
export function normalizeUser(user: User): User {
return {
id: user.id,
name: user.name.trim(),
email: user.email.toLowerCase()
}
}
export function isValidEmail(email: string): boolean {
return email.includes("@")
}
We now have:
- real TypeScript types
- typed exports
- no runtime changes
- full editor support
Step 7 — Update Imports in Other Files¶
If another JS file imports this module:
const { normalizeUser } = require("./userUtils")
It still works because TypeScript emits CommonJS by default.
If using ES modules:
import { normalizeUser } from "./userUtils.js"
Everything remains compatible.
Step 8 — Compile and Run¶
Compile:
npx tsc
Run:
node dist/userUtils.js
You’ve migrated a JS module to TS with:
- zero breaking changes
- minimal edits
- no rewrites
- full type safety
Step 9 — Optional: Increase Strictness Later¶
Once the project stabilizes, enable:
"strict": true
Or incrementally:
noImplicitAnystrictNullChecksnoUncheckedIndexedAccess
This tightens type safety without overwhelming the migration.
What You Learned¶
This hands‑on exercise demonstrates a realistic, low‑friction migration path:
1. Start with JS¶
- keep everything working
- no renaming required
2. Add JSDoc types¶
- instant type safety
- no syntax changes
3. Enable checkJs¶
- TypeScript validates JS files
4. Rename to .ts¶
- minimal code changes
- types become first‑class
5. Replace JSDoc with TS types¶
- full TypeScript benefits
- no runtime changes
This is exactly how real teams migrate large JavaScript codebases safely and incrementally.