TypeScript Introduction
TypeScript is a programming language built by Microsoft as a typed superset of JavaScript: every valid JavaScript program is (almost) valid TypeScript, but TypeScript adds an optional static type system on top. You write TypeScript, and a compiler called tsc checks your types and then strips them away, producing plain JavaScript that runs anywhere JavaScript already runs — browsers, Node.js, Deno, and more. The whole point is to catch a large class of bugs (wrong argument types, typos in property names, forgetting to handle null) at compile time, before a user ever sees them.
Overview / How TypeScript Works
JavaScript is dynamically typed: a variable’s type is only known when the code actually runs, and the engine will happily let you call .toUpperCase() on a number if your logic accidentally hands it one — you only find out when it crashes. TypeScript adds a static type system: you (or the compiler, via inference) declare what type of value a variable, parameter, or return value should hold, and a separate tool, the TypeScript compiler, analyzes your entire program before it runs and reports any place where those types don’t line up.
Three ideas make this system work:
- Static analysis, not runtime checks. Type checking happens once, during compilation. TypeScript does not insert any runtime type checks into your code — there is no performance cost when the program actually executes.
- Type erasure. After checking your code,
tscdeletes every type annotation, interface, and type alias, and emits ordinary JavaScript. The compiled output has no idea what aninterfaceeven is — types exist purely to help you while writing and reading code. - Structural typing. TypeScript compares types by their shape (which properties and methods they have), not by name. If two unrelated objects have the same shape, TypeScript treats them as compatible — this is different from nominally-typed languages like Java or C#.
TypeScript also supports type inference: you often don’t need to annotate every variable, because the compiler figures out the type from the assigned value or from context (like a function’s return statements). You annotate mainly at the boundaries of your code — function parameters, public APIs, and places where inference can’t figure things out on its own.
Because TypeScript compiles down to JavaScript, it isn’t a new runtime or a competing language ecosystem — it’s a development-time layer. You can adopt it file-by-file in an existing JavaScript project, and it integrates with the same npm ecosystem, the same Node.js and browser APIs, and the same tooling you already use.
Syntax
The core syntax addition is the type annotation, written as a colon followed by a type, after a variable name, parameter, or function signature:
let variableName: Type = value;
function functionName(param: ParamType): ReturnType {
// ...
}
| Piece | Meaning |
|---|---|
variableName: Type |
Declares that this variable may only ever hold a value of Type. |
param: ParamType |
Restricts what callers may pass as that argument. |
: ReturnType after the parameter list |
Declares what the function must return; tsc checks every return statement against it. |
interface / type |
Names a reusable shape for objects, so you don’t repeat the same annotation everywhere. |
Common built-in types you’ll use immediately include string, number, boolean, null, undefined, arrays (number[] or Array<number>), and object shapes described with interface or type. Later lessons in this course cover each of these in depth.
Examples
Example 1: Basic type annotations
let username: string = "Ada";
let age: number = 36;
function greet(name: string, age: number): string {
return `Hello, ${name}! You are ${age} years old.`;
}
console.log(greet(username, age));
Output:
Hello, Ada! You are 36 years old.
Here username and age are explicitly typed, and greet‘s parameters and return value are typed too. If you tried to call greet(username, "36"), tsc would refuse to compile, because a string was passed where a number is required — that bug is caught before the code ever runs.
Example 2: A realistic object shape with interface
interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
}
function formatProduct(product: Product): string {
const status = product.inStock ? "In stock" : "Out of stock";
return `${product.name} - $${product.price.toFixed(2)} (${status})`;
}
const laptop: Product = {
id: 1,
name: "Laptop",
price: 999.99,
inStock: true,
};
console.log(formatProduct(laptop));
Output:
Laptop - $999.99 (In stock)
The Product interface documents the exact shape an object must have to be used as a product. Any object literal assigned to a Product-typed variable is checked against every field — miss a field, misspell one, or use the wrong type for one, and tsc reports an error pointing at the exact line.
Example 3: Type inference needs no annotations
function double(x: number) {
return x * 2;
}
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(double);
console.log(doubled);
Output:
[ 2, 4, 6, 8, 10 ]
Notice double has no return-type annotation, and numbers and doubled have no annotations at all — TypeScript infers double returns number, infers numbers is number[], and infers doubled is also number[] because that’s what Array.prototype.map produces when given a function that returns number. This is why idiomatic TypeScript looks far less cluttered than you might expect — you lean on inference and only annotate where it matters.
Under the Hood: What the Compiler Actually Does
When you run tsc on a .ts file, three things happen in sequence:
- Parsing. Your source is parsed into an abstract syntax tree, same as any JavaScript engine would do, except the parser also understands type syntax (
: Type,interface, generics, and so on). - Type checking. The compiler walks the tree, infers or reads the declared type of every expression, and verifies that every assignment, function call, and property access is compatible with those types. This is where errors like “Argument of type ‘string’ is not assignable to parameter of type ‘number'” come from.
- Emit (type erasure). If checking passes (or even if it doesn’t, unless you configure otherwise), the compiler strips out every type annotation,
interface, andtypealias and writes plain JavaScript to disk. Nothing about types survives into the output file — open the compiled.jsfile for any example above and you’ll see ordinary JavaScript with no trace of: stringorinterface Productanywhere.
This matters practically: you cannot check a value’s TypeScript type at runtime (there’s nothing left to check), and a type error never by itself throws an exception while your program runs — it’s caught earlier, while you’re still writing the code, by the editor or the build step.
Common Mistakes
Mistake 1: Assigning a value of the wrong type
let count: number = "5";
This fails to compile with an error like Type 'string' is not assignable to type 'number'. The string "5" looks numeric to a human, but TypeScript compares actual types, not what a value “looks like”. Fix it by using a real number literal, or by converting the string first:
let count: number = 5;
// or, if the value truly starts as a string:
let countFromInput: number = Number("5");
Mistake 2: Reaching for any to silence errors
function processData(data: any) {
return data.toUpperCase();
}
processData(42);
This actually compiles cleanly — and that’s the trap. any tells the compiler “stop checking this value’s type,” so data.toUpperCase() is never flagged even though a number has no toUpperCase method. The bug survives compilation and only surfaces at runtime as TypeError: data.toUpperCase is not a function, which defeats the entire purpose of using TypeScript. Prefer unknown when you genuinely don’t know the type yet, and narrow it before using it:
function processDataSafe(data: unknown): string {
if (typeof data === "string") {
return data.toUpperCase();
}
throw new Error("Expected a string");
}
console.log(processDataSafe("hello"));
Output:
HELLO
With unknown, the compiler forces you to check the value’s actual type (with typeof, instanceof, or a custom type guard) before you’re allowed to call any method on it, so this class of runtime crash becomes a compile-time error instead.
Best Practices
- Enable
"strict": trueintsconfig.jsonfrom day one — it turns on the checks (like disallowing implicitanyand requiring null checks) that catch the most real bugs. - Let inference do the work for local variables; reserve explicit annotations for function parameters, return types on public functions, and places TypeScript can’t infer on its own.
- Avoid
any; preferunknownplus a narrowing check when a type is genuinely not known ahead of time. - Model real-world data with
interfaceortypeso mistakes in object shape are caught where the object is created, not deep inside some unrelated function. - Remember that types vanish at runtime — never rely on a TypeScript type as a way to validate data coming from outside your program (a network response, user input, a file); validate that with real runtime code.
- Run
tsc --noEmitas a fast check-only step in CI, separate from your actual build/bundle step.
Practice Exercises
- Exercise 1: Write a function
calculateArea(width: number, height: number): numberthat returns the area of a rectangle, and call it with two numbers to confirm it compiles and logs the correct result. - Exercise 2: Define an
interface Userwithid: number,name: string, andemail: string. Write a functiondescribeUser(user: User): stringthat returns a sentence describing the user, then create aUserobject and pass it in. - Exercise 3: Rewrite a function that currently takes a parameter typed
anyso that it takesunknowninstead, and add the narrowing logic (usingtypeof) needed to make it compile understrictmode.
Summary
- TypeScript is a typed superset of JavaScript: it adds an optional static type system, then compiles down to plain JavaScript.
- Type checking happens entirely at compile time via
tsc; it adds zero runtime overhead and zero runtime type information. - Types are erased during compilation — the emitted JavaScript looks just like JavaScript you’d write by hand.
- TypeScript uses structural typing (shape-based) and strong type inference, so you don’t need to annotate everything.
- Prefer specific types and
unknownoveranyto keep the compiler’s safety net intact.
