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
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:
- Functions that always throw errors:
function fail(message: string): never {
throw new Error(message);
}
- 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
}
}
- 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:
- typeof:
function print(x: string | number) {
if (typeof x === "string") {
console.log(x.toUpperCase()); // x: string
}
}
- instanceof:
if (animal instanceof Dog) {
animal.bark(); // animal: Dog
}
- in operator:
if ("swim" in animal) {
animal.swim(); // animal: Fish
}
- 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