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:
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.
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:
npm install --save-dev typescript
npx tsc --init # generates a starter 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:
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
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:
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:
// 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'
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
Convert a small JavaScript module to TypeScript
Set up a real project and add types to functions you'd otherwise leave untyped.
Requirements:
- Create a new folder, run
npm init -y, installtypescriptas a dev dependency, and generate atsconfig.jsonwithstrict: true, ansrcrootDirand adistoutDir. - In
src/cart.ts, write a typedpriceWithTax(price: number, taxRate?: number): numberfunction (default the tax rate to0.08inside the function body if omitted). - Write a typed
cartTotal(prices: number[]): numberfunction usingreduce. - Deliberately call one of your functions with a wrong-typed argument (e.g. a string where a number is expected), and confirm
npx tscreports a compile error — then fix the call. - Run
npx tscsuccessfully and confirm plain.jsfiles appear indist/.
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?
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?
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?
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?
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.