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, tsc deletes every type annotation, interface, and type alias, and emits ordinary JavaScript. The compiled output has no idea what an interface even 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, and type alias and writes plain JavaScript to disk. Nothing about types survives into the output file — open the compiled .js file for any example above and you’ll see ordinary JavaScript with no trace of : string or interface Product anywhere.

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": true in tsconfig.json from day one — it turns on the checks (like disallowing implicit any and 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; prefer unknown plus a narrowing check when a type is genuinely not known ahead of time.
  • Model real-world data with interface or type so 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 --noEmit as 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): number that 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 User with id: number, name: string, and email: string. Write a function describeUser(user: User): string that returns a sentence describing the user, then create a User object and pass it in.
  • Exercise 3: Rewrite a function that currently takes a parameter typed any so that it takes unknown instead, and add the narrowing logic (using typeof) needed to make it compile under strict mode.

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 unknown over any to keep the compiler’s safety net intact.