TypeScript Blitz - Day 1: Type System Fundamentals

Learn TypeScript type system fundamentals in 50 minutes - master any vs unknown, union types, type guards, and literal types with practical exercises

15 min read

Goal

After this lesson you’ll understand TypeScript’s type system and confidently type variables and functions - the foundation for everything else.


Theory Part (15 min)

1. Basic Types - Quick Reminder

// Types you know from other languages
let name: string = "John";
let age: number = 25;
let isActive: boolean = true;
let numbers: number[] = [1, 2, 3];
let tuple: [string, number] = ["John", 25];

// null and undefined are separate types
let nothing: null = null;
let notDefined: undefined = undefined;

2. any vs unknown vs never - KEY DIFFERENCE

// ❌ any - disables type checking (AVOID!)
let chaos: any = 5;
chaos = "text";
chaos.nonExistentMethod(); // Compiles, crashes at runtime!

// ✅ unknown - "safe any"
let safe: unknown = 5;
safe = "text";
// safe.toUpperCase(); // ❌ Error! Must check type first

if (typeof safe === "string") {
  safe.toUpperCase(); // ✅ OK - TypeScript knows it's a string
}

// never - type that never occurs
function throwError(message: string): never {
  throw new Error(message);
  // Function never returns a value, always throws
}

function infiniteLoop(): never {
  while (true) {} // Never ends
}

Golden Rule: Never use any - always unknown if you don’t know the type!


3. Union Types - Multi-type Variables

// Variable can have one of several types
let id: string | number;
id = "abc123";  // ✅
id = 12345;     // ✅
// id = true;   // ❌ Error!

// Function parameter
function printId(id: string | number) {
  console.log("ID:", id);
}

// But watch out! Can't use methods without type checking
function formatId(id: string | number): string {
  // return id.toUpperCase(); // ❌ Error! number doesn't have toUpperCase
  
  // ✅ Type Guard - narrows the type
  if (typeof id === "string") {
    return id.toUpperCase(); // Here TS knows it's string
  } else {
    return id.toString(); // Here it knows it's number
  }
}

4. Type Inference - TypeScript Guesses for You

// You don't always need to write types - TS infers them
let message = "Hello"; // TS knows it's string
let count = 42;        // TS knows it's number

// Inference in functions
function add(a: number, b: number) {
  return a + b; // TS knows it returns number
}

let result = add(5, 3); // result has type number

// But sometimes you need to help
let data; // type: any (TS doesn't know)
data = "text";

let data2: string; // Better - explicit type
data2 = "text";

5. Literal Types - Specific Values as Types

// Type is not "string", but a specific value!
let direction: "left" | "right" | "up" | "down";
direction = "left";  // ✅
// direction = "forward"; // ❌ Error!

// Super useful in functions
function move(direction: "left" | "right" | "up" | "down") {
  console.log(`Moving ${direction}`);
}

move("left");     // ✅
// move("forward"); // ❌ Error at compile time!

// as const - freezes values as literals
const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000
} as const;

// config.timeout = 3000; // ❌ readonly!
// typeof config.apiUrl is "https://api.example.com", not "string"

6. Type Guards - Narrowing Types

// typeof guard
function process(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // value: string
  } else {
    console.log(value.toFixed(2)); // value: number
  }
}

// Truthiness guard
function print(text: string | null | undefined) {
  if (text) { // Checks if not null/undefined
    console.log(text.toUpperCase()); // text: string
  }
}

// instanceof guard
class Dog { bark() {} }
class Cat { meow() {} }

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark(); // animal: Dog
  } else {
    animal.meow(); // animal: Cat
  }
}

// 'in' operator guard
type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // animal: Fish
  } else {
    animal.fly(); // animal: Bird
  }
}

Practice Part (35 min)

Exercise 1: API Response Parser (15 min)

Write a function that parses an API response. The API can return different data types and your function must handle them safely.

// User types
type User = {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
};

/**
 * Function checks if unknown data is a valid User
 * Returns User or null if data is invalid
 */
function parseUser(data: unknown): User | null {
  // TODO: Implement validation
  // Hints:
  // 1. Check if data is an object (typeof data === "object" && data !== null)
  // 2. Check if it has required fields
  // 3. Check if fields have correct types
  // 4. Return User or null
}

// Tests - uncomment after implementation
// const validData = { id: 1, name: "John", email: "john@example.com", isActive: true };
// console.log(parseUser(validData)); // Should return User

// const invalidData = { id: "abc", name: 123 };
// console.log(parseUser(invalidData)); // Should return null

// console.log(parseUser(null)); // null
// console.log(parseUser("text")); // null

Solution:

Click to see solution
function parseUser(data: unknown): User | null {
  // Check if it's an object at all
  if (typeof data !== "object" || data === null) {
    return null;
  }

  // Type assertion - now we know it's an object
  const obj = data as Record<string, unknown>;

  // Check each field
  if (
    typeof obj.id === "number" &&
    typeof obj.name === "string" &&
    typeof obj.email === "string" &&
    typeof obj.isActive === "boolean"
  ) {
    return {
      id: obj.id,
      name: obj.name,
      email: obj.email,
      isActive: obj.isActive,
    };
  }

  return null;
}

Exercise 2: API Response Types (10 min)

Create types for different API response states - success, error, loading.

// TODO: Define types for different API states
// 1. SuccessResponse - contains: status: "success", data: any type (generic!)
// 2. ErrorResponse - contains: status: "error", error: string
// 3. LoadingResponse - contains: status: "loading"
// 4. ApiResponse - union of the above three

// Example usage:
function handleResponse(response: ApiResponse) {
  // TODO: Use type guards to handle different cases
  // Hint: check response.status
}

// Tests
// const success: ApiResponse = { status: "success", data: { id: 1 } };
// const error: ApiResponse = { status: "error", error: "Not found" };
// const loading: ApiResponse = { status: "loading" };

// handleResponse(success);
// handleResponse(error);
// handleResponse(loading);

Solution:

Click to see solution
type SuccessResponse<T> = {
  status: "success";
  data: T;
};

type ErrorResponse = {
  status: "error";
  error: string;
};

type LoadingResponse = {
  status: "loading";
};

type ApiResponse<T = unknown> = SuccessResponse<T> | ErrorResponse | LoadingResponse;

function handleResponse<T>(response: ApiResponse<T>) {
  // Discriminated union - status is the "discriminator"
  if (response.status === "success") {
    console.log("Data:", response.data); // TS knows data is here
  } else if (response.status === "error") {
    console.log("Error:", response.error); // TS knows error is here
  } else {
    console.log("Loading..."); // status === "loading"
  }
}

Exercise 3: Type Guards in Practice (10 min)

// You have form data - can come as string or already parsed
type FormData = {
  age: string | number;
  salary: string | number;
  tags: string | string[];
};

/**
 * Function normalizes form data to number/array
 */
function normalizeFormData(data: FormData): {
  age: number;
  salary: number;
  tags: string[];
} {
  // TODO: Implement
  // Hints:
  // - if age is string, parse to number (Number() or parseInt())
  // - if tags is string, do split(',')
  // - use type guards (typeof)
}

// Test
// const formData: FormData = {
//   age: "25",
//   salary: 50000,
//   tags: "typescript,javascript,react"
// };
// console.log(normalizeFormData(formData));
// Should return: { age: 25, salary: 50000, tags: ["typescript", "javascript", "react"] }

Solution:

Click to see solution
function normalizeFormData(data: FormData): {
  age: number;
  salary: number;
  tags: string[];
} {
  return {
    age: typeof data.age === "string" ? Number(data.age) : data.age,
    salary: typeof data.salary === "string" ? Number(data.salary) : data.salary,
    tags: typeof data.tags === "string" ? data.tags.split(",") : data.tags,
  };
}

Quick Review - Key Concepts

1. What’s the difference between any and unknown?

Answer

any completely disables type checking - you can do anything with it, but you lose type safety. The compiler checks nothing.

unknown is “safe any” - it forces you to check the type before using it (type guard). I use unknown when I genuinely don’t know the data type upfront, e.g., parsing JSON from an API.

Example:

let a: any = 5;
a.toUpperCase(); // Compiles, crashes at runtime

let b: unknown = 5;
// b.toUpperCase(); // Compilation error!
if (typeof b === "string") {
  b.toUpperCase(); // OK
}

2. When to use the never type?

Answer

never represents values that never occur. I use it in three cases:

  1. Functions that always throw errors:
function fail(message: string): never {
  throw new Error(message);
}
  1. Exhaustive checking in switch/if:
type Status = "success" | "error";
function handle(status: Status) {
  if (status === "success") { /*...*/ }
  else if (status === "error") { /*...*/ }
  else {
    const _exhaustive: never = status; // Error if we add new status
  }
}
  1. Unreachable code:
function infiniteLoop(): never {
  while (true) {}
}

3. What is a type guard and give an example?

Answer

Type guard is an expression that narrows a type in a given scope. TypeScript uses flow analysis to understand what type a variable is.

Types of type guards:

  1. typeof:
function print(x: string | number) {
  if (typeof x === "string") {
    console.log(x.toUpperCase()); // x: string
  }
}
  1. instanceof:
if (animal instanceof Dog) {
  animal.bark(); // animal: Dog
}
  1. in operator:
if ("swim" in animal) {
  animal.swim(); // animal: Fish
}
  1. Custom type predicate (you’ll learn later):
function isString(x: unknown): x is string {
  return typeof x === "string";
}

4. What are literal types for?

Answer

Literal types allow defining a type as specific values instead of general string or number. They provide type safety at the value level.

Benefits:

  • Autocomplete in IDE
  • Catch errors at compile time
  • Documentation in code - it’s clear what values are allowed

Example:

type Direction = "left" | "right" | "up" | "down";

function move(dir: Direction) { /*...*/ }

move("left");     // ✅
move("forward");  // ❌ Compilation error!

I often use them in configurations, status codes, event types, etc.

5. What is discriminated union and how does it work?

Answer

Discriminated union is a union type with a common field (discriminator) that allows TypeScript to narrow the type.

Structure:

type SuccessResponse = {
  status: "success";  // discriminator
  data: string;
};

type ErrorResponse = {
  status: "error";    // discriminator
  error: string;
};

type Response = SuccessResponse | ErrorResponse;

How it works:

function handle(response: Response) {
  if (response.status === "success") {
    console.log(response.data);   // TS knows it's SuccessResponse
  } else {
    console.log(response.error);  // TS knows it's ErrorResponse
  }
}

The discriminator (status field) must:

  • Exist in all variants
  • Have literal types (not just string)
  • Have different values for each variant

When to use:

  • API responses with different states
  • Event systems with different event types
  • State machines
  • Any scenario with multiple exclusive variants

Checklist - What You Should Know After Day 1

  • Explain the difference between any, unknown, never
  • Use union types (string | number)
  • Write type guards (typeof, instanceof, in)
  • Understand type inference
  • Apply literal types
  • Parse unknown data safely
  • Use discriminated unions (via discriminator field)

Quick Reference Card

// Don't use:
let data: any;

// Use:
let data: unknown;
if (typeof data === "string") {
  // now data: string
}

// Union + Type Guard
function process(x: string | number) {
  if (typeof x === "string") {
    // x: string
  } else {
    // x: number
  }
}

// Literal types
type Status = "idle" | "loading" | "success" | "error";

// Discriminated union
type Response = 
  | { status: "success"; data: T }
  | { status: "error"; error: string };

// Type guards
typeof x === "string"
x instanceof Dog
"swim" in animal

Tomorrow: Interfaces vs Types - modeling real data structures

Back to blog