-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
78: Converting a Small JS Library to TypeScript
Most of the time, you aren't starting a project from a blank slate. You're inheriting a legacy codebase or, more likely, looking at a JS utility library you wrote two years ago and realizing you have no idea what the options object in initConfig() is actually supposed to contain. Converting existing JS to TS is a specific skill—it's more about archaeology than architecture.
To show you how I handle this, let's take a tiny "Shopping Cart" library I wrote in plain JavaScript. It's simple, but it has enough "magic" to make TypeScript grumpy if we don't handle it right.
The Messy Starting Point
Here is the original cart.js. It's functional, but notice how the items array is just... there. I'm assuming the items have an id and a price, but nothing enforces that. If someone passes a string for the price, the calculateTotal function will start concatenating strings instead of adding numbers. That's a classic JS nightmare.
// cart.js
let cart = [];
export function addItem(item) {
cart.push(item);
}
export function calculateTotal(taxRate) {
const subtotal = cart.reduce((acc, item) => acc + item.price, 0);
return subtotal + (subtotal * taxRate);
}
export function clearCart() {
cart = [];
}
My first move is always the same: rename the file to .ts. Don't try to rewrite the logic yet. Just change the extension and let the compiler scream at you. Once I did that here, TypeScript immediately flagged item.price inside the reducer as an error because cart is implicitly any[].
Defining What a "Product" Actually Is
I could just slap any on everything to make the red lines go away, but that defeats the whole purpose of this exercise. I need to define the "shape" of my data. Since a cart item is a fixed structure, an interface is the way to go.
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
let cart: CartItem[] = [];
export function addItem(item: CartItem) {
cart.push(item);
}
Now the addItem function is guarded. If I try to push a random object, the build fails. This is where the real value starts to show up—I've just documented the requirements of the library using code instead of a README that will inevitably be out of date in three weeks.
Fighting with the Reducer (and Winning)
Now let's look at the total calculation. I tried to just add the type to the taxRate parameter, but I hit a snag. Here is where I made a mistake: I forgot that reduce can be finicky with type inference when the array starts empty.
I initially wrote this:
// My first (wrong) attempt
export function calculateTotal(taxRate: number) {
const subtotal = cart.reduce((acc, item) => acc + item.price, 0);
return subtotal + (subtotal * taxRate);
}
Wait, why did that work? In this specific case, because I provided 0 as the initial value, TS inferred acc as a number. But as soon as I tried to make the accumulator more complex (like returning an object with subtotal and tax), TS lost the plot and started complaining that acc was any. I learned the hard way that when using reduce in TS, it's always safer to explicitly type the accumulator if the logic is anything more than basic addition.
Here is the refined, professional version:
export function calculateTotal(taxRate: number): number {
const subtotal = cart.reduce<number>((acc, item) => {
return acc + (item.price * item.quantity);
}, 0);
return subtotal + (subtotal * taxRate);
}
Note the <number> after reduce. That's a generic. I'm telling TypeScript: "The value this reducer produces will be a number." It's a small detail, but it prevents a whole category of "undefined is not a function" errors down the road.
Polishing the Public API
The final step is deciding how the outside world interacts with this. Right now, cart is a global variable in the module. That's a bit risky. I want to make sure that if I ever export the cart for viewing, it can't be modified directly from the outside.
I'll use the ReadonlyArray type. It's a great tool for library authors who want to say, "You can look, but you can't touch."
export function getCartItems(): ReadonlyArray<CartItem> {
return cart;
}
Now, if a user of my library tries to do getCartItems().push(newItem), TypeScript will stop them. I've effectively turned a loose JS script into a predictable, type-safe module.
📋 Practical Task
Convert a Type-Safe Notification Dispatcher
You've been handed a small JavaScript utility called notifier.js. It handles sending messages to different channels (e.g., 'email', 'sms', 'slack'). Currently, it's prone to bugs because there's no validation on the channel names or the message structure.
Your goal: Convert this to TypeScript.
// notifier.js
const channels = {
email: (msg) => console.log(`Sending Email: ${msg}`),
sms: (msg) => console.log(`Sending SMS: ${msg}`),
slack: (msg) => console.log(`Posting to Slack: ${msg}`)
};
export function sendNotification(channel, message) {
if (channels[channel]) {
channels[channel](message);
} else {
throw new Error("Invalid channel");
}
}
Requirements:
- Create a
NotificationChanneltype (using a union of string literals) so that only 'email', 'sms', or 'slack' are permitted. - Type the
channelsobject so that it maps those specific channels to functions that accept a string and return nothing. - Type the
sendNotificationfunction parameters and return type. - Ensure that the
channels[channel]lookup is type-safe and doesn't rely onany.
There are no comments for now.