Week 11: TypeScript Setup & Basic Types

Ten weeks of JavaScript, and every bug that ever came from passing the wrong kind of value to a function was caught only by running the code and seeing what broke. TypeScript adds a type system on top of everything learned so far — the exact same JavaScript underneath, plus a compiler that catches an entire category of bugs before the code ever runs.

Phase 7 of 8 Week 11 of 14 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Set up a minimal tsconfig.json and compile a TypeScript file to JavaScript
  • Use TypeScript's basic types, and know when to let inference handle it instead
  • Add type annotations to a function's parameters and return value

1. Why TypeScript?

TypeScript is JavaScript with an optional type system layered on top, developed by Microsoft and compiled ("transpiled") down to plain JavaScript before it runs — the browser and Node never see TypeScript at all, only the JavaScript it produces:

a bug TypeScript catches before the code ever runs
function add(a: number, b: number) {
  return a + b;
}

add(2, 3);      // fine
add(2, "3");    // TypeScript ERROR at compile time:
                // Argument of type 'string' is not assignable to parameter of type 'number'

In plain JavaScript, add(2, "3") wouldn't error at all — it would silently return "23" (string concatenation, from Week 1's type coercion) and the bug might not surface until much later, far from where it actually happened. TypeScript's compiler catches the mismatch immediately, at the exact line where the wrong type was passed, before the code ever runs.

Types are erased at compile time

None of TypeScript's type annotations exist anymore in the compiled JavaScript output — they're purely a development-time tool. This means TypeScript can never make code faster or slower at runtime; its entire value is catching mistakes before the code ships.

2. Setting Up tsconfig.json

tsconfig.json configures the TypeScript compiler for a project — a minimal but solid starting point:

installing & initializing
npm install --save-dev typescript
npx tsc --init   # generates a starter tsconfig.json
a minimal, strict tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",        // which JS version to compile down to
    "module": "ESNext",         // use ES modules (Week 9) in the output
    "strict": true,              // enable ALL strict type-checking options
    "outDir": "./dist",          // where compiled .js files go
    "rootDir": "./src"           // where your .ts source files live
  },
  "include": ["src/**/*"]
}

"strict": true is worth calling out specifically: it turns on TypeScript's full set of safety checks at once (including disallowing implicit any, covered next). Starting a new project with strict mode on from day one is the standard recommendation — retrofitting it onto a large, already-loose codebase later is considerably more painful than starting with it enabled.

3. Basic Types

TypeScript's fundamental types map directly onto the values you already know from plain JavaScript:

primitive & array types
let username: string = "Priya";
let age: number = 29;
let isActive: boolean = true;

let scores: number[] = [95, 88, 76];        // an array of numbers
let names: Array<string> = ["Ada", "Bo"];  // equivalent generic syntax

let coordinates: [number, number] = [10, 20]; // a TUPLE -- fixed length, fixed types per slot
any vs. unknown
let loose: any = "hello";
loose = 42;          // allowed -- any disables type checking entirely for this variable
loose.toUpperCase(); // allowed too, even though this would crash at runtime now

let safer: unknown = "hello";
safer = 42;                // allowed -- unknown can also hold anything
// safer.toUpperCase();    // TypeScript ERROR: must narrow the type before using it
if (typeof safer === "string") {
  safer.toUpperCase(); // fine now -- TypeScript knows it's a string here
}

A tuple is an array with a fixed length and a specific type for each position — useful for something like a coordinate pair, where "an array of two numbers" isn't quite as precise as "always exactly a number, then a number." any completely opts a value out of type checking — useful briefly while migrating old JavaScript, but it silently defeats the entire point of TypeScript wherever it's used. unknown is the safer alternative: it also accepts any value, but forces you to check what it actually is (narrowing, covered fully in Week 13) before doing anything with it.

4. Inference vs. Explicit Annotation

TypeScript can usually figure out a variable's type on its own, from the value assigned to it — writing the type out by hand isn't always necessary:

inference figures this out automatically
let count = 5; // TypeScript infers: number, no annotation needed

count = "five"; // ERROR: Type 'string' is not assignable to type 'number'
                // -- even though `count` was never explicitly typed!

Even without writing : number, TypeScript infers count's type from its initial value and enforces it from then on — the explicit annotation and the inferred type behave identically in every practical way. A useful rule of thumb: let inference handle simple local variables with an obvious initial value; add explicit annotations on function parameters (which have no initial value for TypeScript to infer from) and anywhere the intended type genuinely isn't obvious from context.

5. Typing Functions

Function parameters need explicit types — there's no initial value for TypeScript to infer them from — but the return type can often be left to inference:

typing a function's parameters & return value
// Parameters MUST be annotated -- there's nothing to infer them from.
// The return type is inferred as `number` here, but can be written explicitly too.
function priceWithTax(price: number, taxRate: number = 0.08): number {
  return price * (1 + taxRate);
}

priceWithTax(100);        // 108 -- fine, taxRate uses its default
priceWithTax(100, "8%");  // ERROR: Argument of type 'string' is not assignable to type 'number'
optional parameters & void
function logMessage(message: string, prefix?: string): void {
  // prefix?: string means prefix is OPTIONAL -- its type is really "string | undefined"
  console.log(prefix ? `${prefix}: ${message}` : message);
}

logMessage("Server started");           // fine, prefix is omitted
logMessage("Server started", "INFO");   // also fine

A ? after a parameter name marks it optional — callers may omit it entirely, and its type inside the function becomes a union with undefined automatically. void is the return type for a function that doesn't return a meaningful value (like one that only logs something) — distinct from undefined, which is a type a function can genuinely return a specific instance of.

6. Hands-on Exercise

Hands-on

Convert a small JavaScript module to TypeScript

Set up a real project and add types to functions you'd otherwise leave untyped.

Requirements:

  1. Create a new folder, run npm init -y, install typescript as a dev dependency, and generate a tsconfig.json with strict: true, an src rootDir and a dist outDir.
  2. In src/cart.ts, write a typed priceWithTax(price: number, taxRate?: number): number function (default the tax rate to 0.08 inside the function body if omitted).
  3. Write a typed cartTotal(prices: number[]): number function using reduce.
  4. Deliberately call one of your functions with a wrong-typed argument (e.g. a string where a number is expected), and confirm npx tsc reports a compile error — then fix the call.
  5. Run npx tsc successfully and confirm plain .js files appear in dist/.
Hint

Add "noEmitOnError": true to compilerOptions so tsc refuses to produce JavaScript output at all while a type error exists — a useful safety net that prevents accidentally shipping code that doesn't type-check.

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Does a browser or Node.js ever directly execute TypeScript code?

No. TypeScript is compiled ("transpiled") down to plain JavaScript before it runs, and it's that generated JavaScript — with every type annotation stripped out — that actually executes in the browser or Node. Types exist purely as a development-time tool for catching mistakes before that compile step.

Q2

What's the practical difference between typing a variable any versus unknown?

Both can hold any value, but any completely disables type checking on it — you can call any method or access any property with no error, even ones that would crash at runtime. unknown still accepts any value but requires you to narrow it (e.g. with a typeof check) before TypeScript will let you actually use it, which keeps the safety net intact.

Q3

In let count = 5; with no type annotation, does TypeScript still catch count = "five" as an error later?

Yes. TypeScript infers count's type as number from its initial value, even without an explicit annotation, and enforces that inferred type exactly the same way it would enforce a written one — assigning a string to it later is still a compile error.

Q4

Why must a function's parameters be explicitly typed, even though local variables often don't need to be?

Inference works by looking at the value assigned to a variable at the point it's declared. A function parameter has no such initial value at the point it's declared — it only receives a value later, at each individual call site — so TypeScript has nothing to infer from and needs the type stated explicitly to check calls against it.