TypeScript Crash Course (Full Notes)
Complete written notes for the TypeScript crash course: setup and tsconfig, primitive types, functions, interfaces vs. types, unions and generics, classes, utility types, enums, and promises.
Full written notes to follow along with the video, or come back to as a reference later. TypeScript is JavaScript with a type system layered on top: it catches a whole category of bugs before your code ever runs, at the cost of an extra compile step.
TypeScript code never runs directly. It compiles down to plain JavaScript, since that's the only thing browsers and Node.js actually understand.
Setup
Install the compiler globally, or per-project if you'd rather pin a version per repo:
npm install -g typescript
tsc --versionInitialize a project and a TypeScript config:
npm init -y
npx tsc --inittsc --init generates tsconfig.json, which controls how your .ts files compile. The settings that matter most starting out:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}rootDir/outDir— where your TypeScript source lives, and where compiled JavaScript gets writtentarget— which JS version the output should compile down tostrict— turns on strict type checking. Leave thistrue; disabling it defeats most of the point of using TypeScript at all
Compile with tsc, then run the compiled output like any other Node file:
tsc
node dist/basics.jsPrimitive types
let username: string = "roadsidecoder";
let age: number = 27;
let isActive: boolean = true;
let scores: number[] = [1, 2, 3, 4];
let names: string[] = ["Alice", "Bob"];Assigning the wrong type is a compile error, not a runtime surprise:
let age: number = "twenty-seven";
// Error: Type 'string' is not assignable to type 'number'.A tuple is a fixed-length array where each position has its own type:
let person: [string, number] = ["Alice", 29];An enum restricts a value to a fixed set of named options:
enum Color {
Red,
Green,
Blue,
}
let favorite: Color = Color.Blue;any disables type checking for a value entirely, which quietly removes the safety TypeScript exists to provide. Prefer unknown when you genuinely don't know a value's type ahead of time; it still forces you to narrow the type before using it:
let userInput: unknown = "hello";
// userInput.length would error here — unknown has no methods until narrowedFunctions
function subscribe(message: string): void {
console.log(message);
}The parameter type comes after the parameter name; the return type comes after the parameter list. void means the function doesn't return a usable value.
An optional parameter uses ?, and default parameters work exactly like plain JavaScript:
function greet(name: string, greeting?: string): string {
return greeting ? `${greeting}, ${name}` : `Hello, ${name}`;
}
function multiply(a: number, b: number = 2): number {
return a * b;
}Rest parameters get typed as an array:
function sum(...nums: number[]): number {
return nums.reduce((total, n) => total + n, 0);
}Arrow functions type the same way, and you can define a reusable function type separately from any specific implementation:
type Calculate = (a: number, b: number) => number;
const add: Calculate = (a, b) => a + b;Objects, interfaces, and types
Typing an object inline gets messy fast, so interface exists to name a shape once and reuse it:
interface User {
name: string;
age: number;
email?: string; // optional
readonly id: number;
}
const user: User = { id: 1, name: "Alice", age: 29 };
user.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.Interfaces can include methods too:
interface Product {
name: string;
price: number;
getDiscount(percent: number): number;
}type can describe the same object shapes, plus things interfaces can't express, like a union of primitives:
type ID = string | number;
const userId: ID = "abc123";
const productId: ID = 42;Interfaces vs. types, the two differences that actually matter day to day:
- Interfaces can be extended; types can't be extended the same way (though types can use intersections to a similar effect).
- Interfaces with the same name in the same scope merge automatically; redeclaring a
typeis a compile error.
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
const rex: Dog = { name: "Rex", breed: "Labrador" };In practice: reach for interface when describing the shape of an object, and type for unions, intersections, or anything that isn't a plain object shape.
Union and intersection types
A union type accepts any one of several types:
type Status = "pending" | "approved" | "rejected";
function setStatus(status: Status): void {
console.log(`Status set to ${status}`);
}A | B — a value matching either shape is valid
An intersection type combines multiple types into one, requiring all of their fields at once:
A & B — a value must satisfy both shapes at once
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
const c: ColorfulCircle = { color: "red", radius: 10 };Literal types
A literal type is a specific value treated as its own type, rather than the general string or number:
let direction: "north" | "south" | "east" | "west" = "north";
direction = "up"; // Error: not assignableThis is the same mechanism behind the Status union above, just written directly on a variable instead of a named type. It's especially useful for modeling API responses that vary by shape:
type SuccessResponse = { data: unknown };
type ErrorResponse = { message: string };
type ApiResponse = SuccessResponse | ErrorResponse;Type assertions and type guards
A type assertion tells the compiler "trust me, I know the real type here" without changing anything at runtime:
const someValue: unknown = "subscribe to roadsidecoder";
const strLength: number = (someValue as string).length;A type guard narrows a union type down to one specific branch, based on a runtime check:
Inside each branch, TypeScript narrows the type automatically — no assertion needed.
function processValue(value: string | number) {
if (typeof value === "string") {
console.log(value.toUpperCase()); // TypeScript knows it's a string here
} else {
console.log(value.toFixed(2)); // and a number here
}
}instanceof works as a type guard for classes:
class Dog {
bark() { console.log("Woof"); }
}
class Cat {
meow() { console.log("Meow"); }
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}Classes
TypeScript classes work like JavaScript classes, plus access modifiers and typed properties:
public — anywhereprotected — class + subclassesprivate — class onlyclass Person {
private name: string;
protected age: number;
public email: string;
constructor(name: string, age: number, email: string) {
this.name = name;
this.age = age;
this.email = email;
}
introduce(): string {
return `Hi, I'm ${this.name}`;
}
get displayName(): string {
return this.name;
}
}private— accessible only inside the class itselfprotected— accessible inside the class and any subclasspublic(the default) — accessible from anywhere
Constructor parameters can declare their access modifier directly, skipping the separate property declarations:
class Employee {
constructor(
private id: number,
public name: string,
protected department: string,
) {}
getDetails(): string {
return `${this.name} works in ${this.department}`;
}
}Inheritance uses extends, and super(...) forwards constructor arguments to the parent class:
class Manager extends Employee {
constructor(
id: number,
name: string,
department: string,
private teamSize: number,
) {
super(id, name, department);
}
getTeamInfo(): string {
return `${this.name} manages a team of ${this.teamSize}`;
}
}A subclass gets every public and protected member of its parent, but not private ones.
Generics
Without generics, a reusable function loses type information the moment it accepts more than one possible type:
function identity(arg: any) {
return arg;
}
const a = identity("hello"); // typed as `any`, not `string`A generic parameter (<T>) lets the function stay reusable while keeping the specific type intact:
Same function, same logic — T is substituted per call instead of being fixed or widened to any.
function identity<T>(arg: T): T {
return arg;
}
const a = identity("hello"); // inferred as string
const b = identity<number>(42); // explicitly providedGenerics work the same way on arrays and interfaces:
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
interface KeyValuePair<K, V> {
key: K;
value: V;
}
const pair: KeyValuePair<string, number> = { key: "age", value: 27 };And on classes:
class DataStorage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
getItems(): T[] {
return this.data;
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem("hello");Generics can be constrained to only accept types with a particular shape:
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello"); // fine — strings have .length
logLength([1, 2, 3]); // fine — arrays have .length
logLength(42); // Error: number has no .lengthUtility types
TypeScript ships a set of built-in helpers for transforming existing types. Starting from:
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: string;
}Partial<T>— makes every field optional, useful for update payloads:type PartialTodo = Partial<Todo>;Required<T>— the opposite, makes every field mandatory:type RequiredTodo = Required<Todo>;Readonly<T>— locks every field after initial assignment:type ReadonlyTodo = Readonly<Todo>;Pick<T, Keys>— keeps only the listed fields:type TodoPreview = Pick<Todo, "title" | "completed">;Omit<T, Keys>— keeps everything except the listed fields:type TodoWithoutDate = Omit<Todo, "createdAt">;Record<Keys, Value>— builds an object type from a union of keys, all sharing one value type:type Page = "home" | "about" | "contact"; interface PageInfo { title: string; url: string; } type Pages = Record<Page, PageInfo>; // { home: PageInfo; about: PageInfo; contact: PageInfo }ReturnType<T>— extracts a function's return type without repeating it manually:function createUser() { return { id: 1, name: "Alice", email: "alice@example.com" }; } type User = ReturnType<typeof createUser>;
Enums, revisited
Numeric enums auto-increment from the first value unless you set one explicitly:
enum Direction {
Up = 1,
Down,
Left,
Right,
}Down is 2, Left is 3, Right is 4 — each one picks up from the previous value.
String enums require every member to have an explicit value, which makes them easier to debug ("pending" instead of 0):
enum Status {
Pending = "pending",
Approved = "approved",
Rejected = "rejected",
}const enum compiles away entirely (no runtime object is generated), which makes it more performant when you don't need to iterate over the enum's values at runtime:
const enum HttpStatus {
OK = 200,
NotFound = 404,
ServerError = 500,
}Promises and async/await
Type the resolved value inside Promise<...>:
function fetchUser(id: number): Promise<{ id: number; name: string }> {
return fetch(`/api/users/${id}`).then((res) => res.json());
}async functions follow the same rule. Promise<void> if nothing meaningful is returned:
async function logUser(id: number): Promise<void> {
const user = await fetchUser(id);
console.log(user);
}Generic async functions type the resolved value the same way any other generic does:
async function getJson<T>(url: string): Promise<T> {
const res = await fetch(url);
return res.json();
}Recap
- TypeScript compiles to JavaScript; it never runs directly, and
tsconfig.jsoncontrols that compile step. strict: trueintsconfig.jsonis what makes TypeScript actually useful — avoidany, and reach forunknownwhen a type is genuinely not known ahead of time.- Interfaces describe object shapes and can be extended or merged;
typecovers everything else, including unions and intersections. - Type guards (
typeof,instanceof) narrow a union down to one branch at a time, based on a runtime check. - Generics keep a function, class, or interface reusable without losing the specific type information for each call.
- Utility types (
Partial,Pick,Omit,Record,ReturnType, and others) transform existing types instead of redefining them by hand.
Want to see TypeScript in a real project?
See how these same TypeScript fundamentals show up in a full-stack Next.js project, end to end.
Watch the full project video