Notifications

No notifications

/Phase 4

TypeScript Basics

TypeScript — JavaScript with Static Types

TypeScript is a superset of JavaScript that adds optional static typing. It catches errors at compile time, improves IDE autocompletion, and serves as living documentation.

Primitive Types

let name: string = "Alice";
let age: number = 25;
let active: boolean = true;
let data: null = null;
let items: string[] = ["a", "b", "c"];

Interfaces vs Types

FeatureInterfaceType Alias
Object shapes
Extendsextends keywordIntersection &
Declaration merging
UnionsA \B
Primitives/Tuples

Generics — Reusable Typed Logic

function identity<T>(value: T): T { return value; }
identity<string>("hello"); // T = string
identity(42);              // T inferred as number

Enums

enum Status { Pending, Active, Closed }
let s: Status = Status.Active; // 1

Type Narrowing

TypeScript narrows types using control flow:

TechniqueExample
typeofif (typeof x === "string")
inif ("name" in obj)
instanceofif (err instanceof Error)
Discriminated unionif (shape.kind === "circle")

Utility Types

Partial, Required, Pick, Omit, Record, Readonly

On this page

Detailed Theory

What TypeScript Actually Does

TypeScript is JavaScript with a type checker bolted on. You write almost the same code, but you also describe the *shape* of your data — and the compiler catches mistakes before the code ever runs.

function double(n: number) { return n * 2; }
double("hi"); // ❌ caught at compile time, before users see a crash

The catch: TypeScript disappears at build time. The browser only sees plain JavaScript. Types are pure documentation enforced by the compiler.

Basic Types

let name: string = "Asha";
let age: number = 21;
let active: boolean = true;
let tags: string[] = ["js", "ts"];
let pair: [number, string] = [1, "x"]; // tuple — fixed length & types
let nothing: null = null;
let unknownValue: unknown;             // safer than any
let anything: any;                     // ⚠ disables type checking

Tip: TypeScript can usually infer types — write let age = 21 and it already knows it's a number. Annotate function parameters and return types; let inference do the rest.

Object Types: type vs interface

Both describe the shape of an object. Either is fine; pick one and be consistent.

type User = {
  id: number;
  name: string;
  email?: string;     // optional
  readonly createdAt: Date;
};

interface User { id: number; name: string; email?: string; }

Tiny differences:

  • interface can be extended: interface Admin extends User { ... }.
  • type can describe anything: unions, intersections, primitives, mapped types.
  • interface declarations with the same name merge; type aliases don't.
Rule of thumb: interface for object shapes you might extend, type for unions and utility types.

Functions

function add(a: number, b: number): number {
  return a + b;
}

const greet = (name: string): string => Hi, ${name};

// optional + default params function log(msg: string, level: "info"

"warn" = "info") { /* … */ }

Union & Literal Types

A union means "this OR that":

let id: string
number; // either is allowed type Status = "idle"
"loading""success"
"error"; // string literal union

String literal unions are amazing for prop options — you get autocomplete for free.

Arrays, Records, and Generics in 30 Seconds

const nums: number[] = [1, 2, 3];
const ages: Array<number> = [21, 22];          // identical
const map: Record<string, number> = { a: 1 };  // object as a dictionary

function first<T>(arr: T[]): T | undefined { return arr[0]; } first<string>(["a", "b"]); // T is string first([1, 2, 3]); // inferred as number

is a generic — a placeholder for any type. It lets you write a function or component that works with many types while keeping safety.

Working with React

type ButtonProps = {
  label: string;
  onClick: () => void;
  variant?: "primary" | "ghost";
};

function Button({ label, onClick, variant = "primary" }: ButtonProps) { return <button onClick={onClick} className={variant}>{label}</button>; }

const [count, setCount] = useState<number>(0); const inputRef = useRef<HTMLInputElement>(null);

For event handlers:

function onChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value);
}

Beginner Mistakes to Skip

1. Reaching for any to silence errors — defeats the entire point. Use unknown instead and narrow it. 2. Annotating everything — TS infers most types. Annotate boundaries (function parameters, returns, exports), not internal variables. 3. Mixing null and undefined randomly — pick a convention. Most code-bases prefer undefined for "missing". 4. Type assertion as everywhere — that's "trust me bro, no runtime check". Reach for type guards instead. 5. Forgetting strict: true in tsconfig.json — without it, half the safety is off.

Intermediate: Structural Typing

TypeScript uses structural typing ("if it walks like a duck"). Two types are compatible if their *shapes* match — names don't matter:

interface Point { x: number; y: number }
const p = { x: 1, y: 2, z: 3 }; // extra property is fine
const point: Point = p;          // ✅ valid

This is why you can pass an object literal that "happens to fit". Watch out for the excess property check: passing a literal *directly* into a typed slot will flag extras.

Intermediate: Narrowing — How TS Sharpens a Type

Inside a guard, TS narrows the type for you:

function show(x: string | number) {
  if (typeof x === "string") {
    x.toUpperCase(); // x is string here
  } else {
    x.toFixed(2);    // x is number here
  }
}

// instanceof if (err instanceof Error) err.message;

// in operator if ("email" in user) user.email;

// custom type guard (predicate) function isUser(x: unknown): x is User { return typeof x === "object" && x !== null && "id" in x; }

Narrowing is the way you make unknown useful — never reach for as first.

Intermediate: Discriminated Unions

Combine a union with a shared literal field for safe pattern matching:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number };

function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.radius ** 2; case "square": return s.side ** 2; } }

This pattern is *the* idiomatic way to model "one of N variants" — Redux actions, API responses, drawing primitives.

Intermediate: Utility Types

TypeScript ships dozens of helpers that transform types. Daily-use ones:

type User = { id: number; name: string; email: string };

Partial<User> // all fields optional Required<User> // all fields required Readonly<User> // all fields readonly Pick<User, "id" | "name"> // subset of fields Omit<User, "email"> // every field except… Record<"a" | "b", number> // { a: number; b: number } ReturnType<typeof fn> // the type fn returns Awaited<Promise<string>> // string

Compose them: Partial> for an "edit user" form.

Intermediate: Generics with Constraints

function getLength<T extends { length: number }>(item: T): number {
  return item.length; // safe — T must have a .length
}

getLength("hi"); // ✅ getLength([1, 2]); // ✅ getLength(42); // ❌ number has no length

Constrain generics whenever you need to use a property of T inside.

Intermediate: unknown vs any

any opts out of type checking — never use it without a really good reason. unknown opts in — you must narrow before using it. Always prefer unknown for parsed JSON, third-party data, etc.

const data: unknown = JSON.parse(raw);
if (typeof data === "object" && data && "id" in data) {
  // safe to use data.id now
}

Advanced: Conditional & Mapped Types

// Conditional: if X extends Y, do A, else B
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">;  // true
type B = IsString<5>;     // false

// Mapped: transform every key type Mutable<T> = { -readonly [K in keyof T]: T[K] };

// keyof + indexed access type Keys = keyof User; // "id"

"name"
"email" type Email = User["email"]; // string

These power the entire utility-type ecosystem and library typings.

Advanced: Template Literal Types

Type-level string manipulation:

type Color = "red" | "blue";
type Variant = bg-${Color}-500; // "bg-red-500" | "bg-blue-500"

Used heavily by libraries like Tailwind plugins and CSS-in-TS.

Advanced: satisfies — The Best of Both Worlds

When you want TS to *check* your literal matches a type, but keep the narrow inferred type:

const config = {
  retries: 3,
  mode: "fast",
} satisfies { retries: number; mode: "fast" 
"slow" };

config.mode; // type is "fast", not "fast"

"slow"

Advanced: Exhaustiveness with never

never means "this can't happen". Use it to prove a switch covers every case:

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.radius ** 2;
    case "square": return s.side ** 2;
    default:
      const _exhaustive: never = s; // compile error if a Shape variant is missed
      return _exhaustive;
  }
}

Add a new variant to Shape later → TS immediately points at the unhandled case.

Practice Path

1. Type a small library: a User interface, an updateUser(id: number, patch: Partial) function, and a discriminated-union Result for success/error. 2. Convert a JS file to TS without using any. Use unknown + narrowing for anything imported from JSON. 3. Build a generic useFetch(url) hook returning { data: T

null; error: Error
null; loading: boolean }. 4. Add an exhaustive switch over a string-literal union and force a missed-case compile error using never.