
Why types feel like a burden (until they don't)
Every June we get a new batch of students who have spent their first year writing JavaScript. They've built small games, a weather app, maybe a chat thing that mostly works. The first week of TypeScript, the complaint is always the same: "I have to write so much extra stuff." They look at a function like this:
function calculateTotal(items: CartItem[], taxRate: number): number {
// ...
}
and they see the same function in JavaScript:
function calculateTotal(items, taxRate) {
// ...
}
The JavaScript version is shorter. They're right. Shorter is real.
The thing they don't see yet is that the TypeScript version carries information. The signature tells you what the function expects and what it returns. You don't have to read the body to know that items is an array of cart items and that you get a number back. In the JavaScript version, you'd have to trace every call site, or guess from the variable names, or trust that someone wrote good comments.
By week three, the same students are writing interfaces before they write the functions that use them. They've felt the difference: the editor autocompletes the properties of an object they're building. The compiler catches a typo in a property name before the code ever runs. The burden flips. The extra typing in the signature saves them from reading entire function bodies when they return to the code a month later.
We can't prove that TypeScript makes code have fewer bugs. The studies are messy and the metrics are contested. But we can say this: TypeScript moves error detection from the browser console to the editor. That alone changes how fast you catch mistakes, and it changes the kind of mistakes you make.
The type system is structural, not nominal
This is the part that confuses people coming from Java or C#. In those languages, a type is a name. A Person and a Customer are different types even if they have exactly the same fields, because they have different names. TypeScript does not work that way.
TypeScript uses structural typing. Two objects are the same type if they have the same shape. That means a function that expects a Customer will accept any object that has the properties a customer has, whether or not that object was ever called a customer.
Here's what that means in practice. You write a function that takes a person with a name and an email. You can pass it an object from a database query, a form submission, an API response — as long as the object has name and email as strings, it works. You don't need every object to formally implement some EmailRecipient interface. This is closer to how JavaScript actually works, where objects are just collections of properties and you don't care about their pedigree.
The downside is that you lose some safety. If two unrelated types happen to have the same shape, TypeScript will treat them as compatible. In most real code, this is fine. It's a trade: you get flexibility without the ceremony of nominal typing, and you occasionally lose the ability to distinguish two types that look the same but mean different things.
We tell students not to fight the structural typing. It's one of the reasons TypeScript feels less foreign than Java. You don't declare types to satisfy a class hierarchy; you declare them to describe shape.
Type inference: write less, trust more
A common beginner mistake is to annotate everything. They write let count: number = 5; and let name: string = "Alice";. The : number on the left side of count is redundant. TypeScript already knows that 5 is a number and "Alice" is a string. Inference means the compiler figures out the type from the value. You only need annotations in places where inference can't work: function parameters, return types when you want to be explicit, and variables that start empty.
The real skill is knowing when to annotate and when to let TypeScript work it out. A good rule of thumb: annotate the boundaries, not the internals. Annotate function parameters and return types, because those are the contracts other code depends on. Don't annotate local variables, because the value itself tells you the type.
There's a specific mistake we see in week two. Students write a function that returns different shapes in different branches:
function getConfig(env: string) {
if (env === "production") {
return { url: "https://api.example.com", debug: false };
}
return { url: "http://localhost:3000" };
}
TypeScript infers the return type as a union of two shapes. That's fine. But then they try to access .debug on the result and the compiler says no, because debug might not exist. The student is annoyed. They thought TypeScript was being stupid. It's not. It's being honest: the function really does return two different shapes, and the caller has no way to know which one they'll get. The fix is to make the return type explicit — a union type that says debug is optional — or to refactor so the shape is consistent.
This is where TypeScript earns its keep. It doesn't just check your code; it forces you to confront the places where your code was already ambiguous.
Narrowing: the type system gets smaller
Type narrowing is a concept that doesn't exist in JavaScript at all, because JavaScript doesn't have types to narrow. The idea is simple: inside a conditional, TypeScript reduces the type of a variable to what's actually possible there.
function formatId(id: string | number) {
if (typeof id === "string") {
return id.toUpperCase();
}
return id.toFixed(2);
}
Inside the first branch, TypeScript knows id is a string. Inside the second, it knows id is a number. This is not magic. It's control flow analysis — the compiler tracks what checks you've done and narrows the type accordingly.
The practical payoff is huge. You can write code that checks for null, checks for undefined, checks the result of Array.isArray, and TypeScript follows along. The code reads like normal JavaScript, but the editor gives you the right autocomplete in each branch.
The place this trips people up is with optional properties. If you have an object with an optional address property, and you check if (user.address), TypeScript narrows user.address to the actual address type inside the block. But if you call a function inside that block that takes the user object as a parameter, TypeScript doesn't narrow the parameter — because the function could theoretically change the object. This is a real limitation, and it confuses people every year. The workaround is to extract the property into a local variable:
const address = user.address;
if (address) {
// address is narrowed here, even inside function calls
}
This works because address is a local constant — nobody else can change it.
Common questions
Does TypeScript make my bundle bigger?
No. TypeScript is a development-time tool. The compiler strips all type annotations when it outputs JavaScript. The generated code is roughly the same size as what you'd write by hand, and sometimes smaller because the compiler can do optimizations. The types themselves never reach the browser.
Do I need to learn TypeScript if I already know JavaScript well?
You don't need to, but the job market increasingly expects it. More importantly, TypeScript will make you a better JavaScript developer because it forces you to think about the shapes of your data. You'll catch mistakes you were making in JavaScript without noticing.
Can I migrate a large JavaScript project to TypeScript gradually?
Yes. The TypeScript compiler has an allowJs option that lets you mix JavaScript and TypeScript files. You can add .ts files to an existing project, set checkJs: false for the old JavaScript files, and tighten the settings as you convert. We've seen teams migrate a 200,000-line codebase over a few months, one module at a time.
What's the difference between interface and type in TypeScript?
They're mostly interchangeable. interface is better for describing object shapes that might be extended or implemented, and type can describe unions, tuples, and other complex types that interface can't. In practice, pick one and be consistent. The compiler treats them almost identically for object shapes.
Courses this relates to
Come and sit a class before you decide
Reading about it only gets you so far. Pick a day, sit in on a class that is already running, write some code, and talk to the students in it. Free, and nothing to pay afterwards unless you want to join.