Skip to main content

🎨 Basic Types in TypeScript

Welcome to the colorful world of TypeScript types! Think of types as different colored LEGO bricks - each one has its purpose, and knowing which brick to use makes building amazing things so much easier. In this lesson, we'll explore TypeScript's fundamental building blocks and learn how to use them like a pro! 🧱

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Master TypeScript's primitive types: string, number, boolean
  • Work confidently with arrays and understand different array syntax
  • Use tuples to represent fixed-length arrays with mixed types
  • Understand when to use any, unknown, never, and void
  • Leverage type inference to write cleaner code
  • Create union types for flexible, type-safe code

Estimated Time: 60-75 minutes

Project: Build a type-safe inventory system using all the basic types

📑 In This Lesson

Primitive Types: The Foundation

Just like learning the alphabet before writing words, we need to understand TypeScript's primitive types before building complex applications. These are the fundamental building blocks - simple, atomic types that can't be broken down further. Let's meet the main characters! 🎭

String: Working with Text

Think of a string as any text you can type - names, messages, descriptions, you name it! In TypeScript, we use the string type for all text data.

TypeScript Primitive Types Visual representation of the three main primitive types: string, number, and boolean string "Hello" 'World' `Template` 📝 Text data number 42, 3.14 -5, 1_000 Infinity, NaN 🔢 Numeric data boolean true false 💡 Yes/No data
// Declare a string variable
let userName: string = "Alice";
let greeting: string = 'Hello, World!';
let message: string = `Welcome, ${userName}!`;  // Template literals work great!

// Strings can be reassigned to other strings
userName = "Bob";           // ✅ OK
userName = 123;             // ❌ Error! Type 'number' is not assignable to type 'string'

💡 String Tips

  • Use template literals (backticks) for strings with variables: `Hello, ${name}!`
  • Both single quotes and double quotes work - pick one style and stick with it
  • Multi-line strings are easy with template literals

Number: All Your Math Needs

Unlike some languages that have separate types for integers and decimals, TypeScript (like JavaScript) has just one number type. It's like a Swiss Army knife for all numeric needs! 🔢

// All these are type 'number'
let age: number = 25;                    // Integer
let price: number = 19.99;               // Decimal
let temperature: number = -5;            // Negative
let bigNumber: number = 1_000_000;       // With underscores for readability
let hex: number = 0xFF;                  // Hexadecimal
let binary: number = 0b1010;             // Binary
let octal: number = 0o744;               // Octal

// Math operations work as expected
let total: number = price * 2;           // 39.98
let average: number = (10 + 20) / 2;     // 15

// Special numeric values
let infinite: number = Infinity;
let notANumber: number = NaN;
🎯 Real-World Example: You're building an e-commerce site. Prices, quantities, ratings, shipping weights - they're all number types. One type to rule them all!

Boolean: True or False

The boolean type is your yes/no, on/off, true/false switch. It's the simplest type but incredibly powerful for logic! Think of it as a light switch - it's either on (true) or off (false). 💡

// Boolean values
let isLoggedIn: boolean = true;
let hasPermission: boolean = false;
let isEmailVerified: boolean = true;

// Booleans from comparisons
let isAdult: boolean = age >= 18;        // true if age is 18 or more
let isEmpty: boolean = userName.length === 0;
let isValid: boolean = price > 0 && price < 1000;

// Using booleans in logic
if (isLoggedIn && hasPermission) {
    console.log("Access granted!");
}

// Common mistake - don't compare booleans with === true
if (isLoggedIn === true) {  // ❌ Redundant
    // do something
}

if (isLoggedIn) {           // ✅ Better - it's already a boolean!
    // do something
}

⚠️ Truthiness vs Boolean

In JavaScript, many values are "truthy" or "falsy" (like empty strings, 0, null), but that doesn't make them booleans. TypeScript keeps these concepts separate:

let count: number = 0;
let message: string = "";

// These are NOT booleans, even though they're "falsy"
let isFalsy: boolean = count;    // ❌ Error!
let isEmpty: boolean = message;   // ❌ Error!

// Convert explicitly if needed
let isFalsy: boolean = count === 0;      // ✅ Correct
let isEmpty: boolean = message === "";   // ✅ Correct

Comparing the Primitives

Let's see these types in action with a practical example:

// A user profile using primitive types
let userId: number = 12345;
let username: string = "codecraftsman";
let email: string = "user@example.com";
let age: number = 28;
let isVerified: boolean = true;
let isPremium: boolean = false;
let accountBalance: number = 1599.50;

// Function using all primitive types
function displayUserInfo(
    id: number,
    name: string,
    verified: boolean,
    balance: number
): string {
    return `User #${id}: ${name} | Verified: ${verified} | Balance: $${balance}`;
}

console.log(displayUserInfo(userId, username, isVerified, accountBalance));
// Output: User #12345: codecraftsman | Verified: true | Balance: $1599.5

Primitive Types Quick Reference

Type Purpose Examples Common Uses
string Text data "Hello", 'World', `Hi ${name}` Names, messages, descriptions, IDs
number Numeric data 42, 3.14, -5, Infinity, NaN Counts, prices, calculations, ages
boolean True/False values true, false Flags, conditions, states, toggles
graph TD A[Primitive Types] --> B[string] A --> C[number] A --> D[boolean] B --> B1["Text: 'Hello'"] B --> B2["IDs: 'user_123'"] B --> B3["Messages"] C --> C1["Integers: 42"] C --> C2["Decimals: 3.14"] C --> C3["Special: NaN, Infinity"] D --> D1["true"] D --> D2["false"] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style B fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff style C fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff style D fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff

Arrays and Tuples: Collections of Values

Now that we know the basic building blocks, let's learn how to organize them! Think of arrays as shopping lists and tuples as specifically ordered packages. Both are incredibly useful in real applications! 📦

Arrays: Lists of the Same Type

An array is a collection of values of the same type. It's like a container that only holds one kind of item - a fruit bowl that only holds apples, or a parking lot that only holds cars. 🍎🚗

Two Ways to Declare Arrays

TypeScript gives you two syntaxes for arrays. They're completely equivalent - use whichever feels more natural to you!

// Method 1: Type followed by []
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];
let flags: boolean[] = [true, false, true];

// Method 2: Array<Type> (generic syntax)
let scores: Array<number> = [95, 87, 92, 88];
let colors: Array<string> = ["red", "green", "blue"];

// Both mean the same thing! Pick your favorite and stick with it.
// Most developers prefer the [] syntax because it's shorter

Working with Arrays

Once you have a typed array, TypeScript ensures you only put the right type of items in it:

let prices: number[] = [9.99, 19.99, 29.99];

// Adding items - must match the type
prices.push(39.99);        // ✅ OK - 39.99 is a number
prices.push("49.99");      // ❌ Error! String is not assignable to number

// Accessing items - TypeScript knows the type
let firstPrice: number = prices[0];        // TypeScript knows this is a number
let upperPrice = prices[1].toFixed(2);     // Can use number methods!

// Array methods work as expected
let total = prices.reduce((sum, price) => sum + price, 0);
let discounted = prices.map(price => price * 0.9);  // 10% off!
let expensive = prices.filter(price => price > 20);

🎯 Real-World Example: Shopping Cart

interface CartItem {
    id: number;
    name: string;
    price: number;
}

let cart: CartItem[] = [
    { id: 1, name: "Laptop", price: 999 },
    { id: 2, name: "Mouse", price: 25 },
    { id: 3, name: "Keyboard", price: 75 }
];

// Calculate total
let cartTotal = cart.reduce((sum, item) => sum + item.price, 0);
console.log(`Total: $${cartTotal}`);  // Total: $1099

Empty Arrays

Sometimes you start with an empty array and add items later. You still need to tell TypeScript what type of items you'll add:

// Declare an empty array with a type
let todoList: string[] = [];

// Now you can add items
todoList.push("Learn TypeScript");     // ✅ OK
todoList.push("Build a project");      // ✅ OK
todoList.push(123);                    // ❌ Error! Expected string

// Without a type annotation, TypeScript might get confused
let mystery = [];                       // Type is any[] - not safe!
mystery.push("anything");               // No error, but no type safety either
mystery.push(123);
mystery.push(true);                     // This is dangerous!

Tuples: Fixed-Length Arrays with Mixed Types

A tuple is like a precisely packed box - it has a fixed number of items, each with a specific type, in a specific order. Think of coordinates (x, y), or a person's info (name, age, isActive). 📍

// Tuple: [string, number]
let user: [string, number] = ["Alice", 25];

// Access by index - TypeScript knows the type at each position!
let name: string = user[0];     // TypeScript knows this is a string
let age: number = user[1];      // TypeScript knows this is a number

// Wrong order or type = error
let invalid: [string, number] = [25, "Alice"];  // ❌ Error! Wrong order

// Wrong number of elements = error
let tooMany: [string, number] = ["Alice", 25, true];    // ❌ Error!
let tooFew: [string, number] = ["Alice"];               // ❌ Error!

When to Use Tuples

Tuples are perfect when you have a fixed structure with mixed types:

// RGB color: [red, green, blue]
let color: [number, number, number] = [255, 128, 0];

// Coordinates: [x, y]
let point: [number, number] = [100, 200];

// API response: [status, message, data]
let response: [number, string, any] = [200, "Success", { id: 1 }];

// Key-value pair
let setting: [string, boolean] = ["darkMode", true];

// Function that returns multiple values
function getUserInfo(): [string, number, boolean] {
    return ["Alice", 25, true];  // [name, age, isVerified]
}

let [userName, userAge, isVerified] = getUserInfo();  // Destructuring!
console.log(`${userName} is ${userAge} years old`);

⚠️ Tuple Gotcha: Array Methods

Tuples are actually just arrays under the hood, so array methods like push() still work - but they can break your tuple's structure!

let pair: [string, number] = ["Alice", 25];

// This works but breaks the tuple contract!
pair.push(true);  // ✅ No error (unfortunately), but now it's [string, number, boolean]

// Better: Use readonly tuples if you want true immutability
let safePair: readonly [string, number] = ["Alice", 25];
safePair.push(true);  // ❌ Error! Property 'push' does not exist on readonly array

Arrays vs Tuples: When to Use Which?

Array vs Tuple Comparison Visual comparison showing arrays have variable length with same types, while tuples have fixed length with specific types at each position Array: number[] 1 2 3 ... n ✅ Variable length ✅ Same type throughout ✅ Can push/pop items Use for: lists, collections Tuple: [string, number] "Alice" string 25 number ✅ Fixed length ✅ Type at each position ✅ Ordered structure Use for: coordinates, pairs
Scenario Use This Example
List of same-type items (unknown length) Array let names: string[] = ["Alice", "Bob", ...]
Fixed structure with mixed types Tuple let user: [string, number] = ["Alice", 25]
Collection that grows/shrinks Array let cart: Product[] = []
Function returning multiple values Tuple function getData(): [string, number]
Coordinates, RGB colors, pairs Tuple let point: [number, number] = [x, y]
graph LR A[Collections] --> B[Array] A --> C[Tuple] B --> B1["Same type items"] B --> B2["Variable length"] B --> B3["string[], number[]"] C --> C1["Mixed types"] C --> C2["Fixed length"] C --> C3["[string, number]"] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style B fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff style C fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff

Type Inference: TypeScript Reads Your Mind

Here's some great news: you don't always have to write type annotations! TypeScript is smart enough to figure out types based on the values you assign. It's like having an assistant who finishes your sentences. 🧠✨

How Type Inference Works

When you assign a value to a variable, TypeScript looks at that value and automatically determines the type:

Click on values to see TypeScript's inferred type!

// TypeScript infers the type from the value
let message = "Hello!";          // TypeScript infers: string
let count = 42;                  // TypeScript infers: number
let isActive = true;             // TypeScript infers: boolean
let items = [1, 2, 3];          // TypeScript infers: number[]
let mixed = ["Alice", 25];       // TypeScript infers: (string | number)[]

// You get the same type safety without writing the type!
message = "Hi there!";           // ✅ OK - string to string
message = 123;                   // ❌ Error! number is not assignable to string

count = 100;                     // ✅ OK
count = "100";                   // ❌ Error!
💡 Best Practice: Let TypeScript infer types when they're obvious. It makes your code cleaner and easier to read. Only add explicit type annotations when inference needs help or when it makes the code clearer.

When Inference Works Best

Type inference shines in these situations:

// ✅ GOOD: Let inference work
let name = "Alice";              // Obviously a string
let age = 25;                    // Obviously a number  
let scores = [95, 87, 92];      // Obviously number[]

// Array methods maintain type safety
let doubled = scores.map(s => s * 2);  // TypeScript knows this is number[]
let high = scores.filter(s => s > 90); // Still number[]

// Object literals
let user = {
    name: "Alice",               // Inferred as string
    age: 25,                     // Inferred as number
    isActive: true               // Inferred as boolean
};

// TypeScript knows the structure!
console.log(user.name.toUpperCase());  // ✅ OK - name is definitely a string
console.log(user.age.toFixed(2));      // ✅ OK - age is definitely a number
console.log(user.email);                // ❌ Error! Property 'email' doesn't exist

When to Add Explicit Types

Sometimes you need to help TypeScript out. Add explicit types in these cases:

1. Empty Arrays

// ❌ TypeScript doesn't know what type will go in here
let items = [];                  // Type: any[] (unsafe!)

// ✅ Tell TypeScript what you intend
let items: string[] = [];        // Type: string[] (safe!)

2. Function Parameters

// ❌ Parameters need explicit types (can't infer from usage)
function greet(name) {           // Error: Parameter 'name' implicitly has 'any' type
    return `Hello, ${name}!`;
}

// ✅ Always type function parameters
function greet(name: string): string {
    return `Hello, ${name}!`;
}

// Return type can often be inferred, but being explicit is good practice
function add(a: number, b: number) {     // Return type inferred as number
    return a + b;
}

3. When You Want a More General Type

// TypeScript infers the literal type "pending"
let status = "pending";          // Type: "pending" (too specific!)

// You might want a more general type
let status: string = "pending";  // Type: string (can be any string)

// Or a union of specific values
let status: "pending" | "approved" | "rejected" = "pending";  // Best!

4. Complex Types

// Type inference can get messy with complex structures
let data = {
    users: [
        { name: "Alice", roles: ["admin", "user"] },
        { name: "Bob", roles: ["user"] }
    ]
};
// The inferred type is huge and hard to understand!

// Better: Define the structure explicitly
interface User {
    name: string;
    roles: string[];
}

let data: { users: User[] } = {
    users: [
        { name: "Alice", roles: ["admin", "user"] },
        { name: "Bob", roles: ["user"] }
    ]
};
// Much clearer what this structure should be!

🎯 The Golden Rule of Type Annotations

If the type is obvious from the value, let TypeScript infer it. If it's not obvious or you want to be explicit about contracts (like function parameters and return types), add the annotation.

// ✅ Good balance
let count = 0;                              // Inferred
let items: string[] = [];                   // Explicit (empty array)
let user = { name: "Alice", age: 25 };     // Inferred

function processUser(user: User): void {    // Explicit (function signature)
    let age = user.age;                     // Inferred
    let nextYear = age + 1;                 // Inferred
}

Type Inference in Action

Let's see a complete example that shows good use of type inference:

// Explicit where it matters
interface Product {
    id: number;
    name: string;
    price: number;
    inStock: boolean;
}

function calculateTotalPrice(products: Product[]): number {
    // Let inference handle local variables
    let total = 0;  // Inferred as number
    
    for (let product of products) {  // product inferred as Product
        if (product.inStock) {
            let price = product.price;     // Inferred as number
            let subtotal = price * 1.1;    // Inferred as number (with 10% markup)
            total += subtotal;
        }
    }
    
    return total;  // TypeScript verifies we return a number
}

// Usage with inferred types
let inventory = [  // Inferred as Product[]
    { id: 1, name: "Laptop", price: 999, inStock: true },
    { id: 2, name: "Mouse", price: 25, inStock: true }
];

let total = calculateTotalPrice(inventory);  // Inferred as number
console.log(`Total: $${total.toFixed(2)}`);  // Can use number methods!

Union Types: This OR That

Sometimes a value can be more than one type. Maybe an ID can be a number OR a string. Maybe a status can be "loading" OR "success" OR "error". Union types let you express this flexibility while keeping type safety! Think of it like ordering at a restaurant: "I'll have the burger OR the pizza." 🍔🍕

Creating Union Types

Use the pipe symbol (|) to create a union type:

Union Types Visualization Visual showing how a union type can accept values from multiple types let id: string | number string "abc123" number 12345 OR Variable can hold either type!
// A variable that can be string OR number
let id: string | number;

id = "abc123";        // ✅ OK - string is allowed
id = 12345;           // ✅ OK - number is allowed
id = true;            // ❌ Error! boolean is not in the union

// Union with multiple types
let result: number | string | boolean;

result = 42;          // ✅ OK
result = "success";   // ✅ OK
result = true;        // ✅ OK
result = null;        // ❌ Error! null is not in the union

Working with Union Types

When you have a union type, TypeScript only lets you use operations that work for ALL types in the union. This is where type narrowing comes in handy:

Type Narrowing with Union Types Visual showing how typeof checks narrow a union type to a specific type id: string | number Could be either! typeof id === ? "string" id: string .toUpperCase() ✓ "number" id: number .toFixed() ✓
function printId(id: string | number) {
    // Can't use string methods or number methods directly
    // console.log(id.toUpperCase());  // ❌ Error! number doesn't have toUpperCase
    // console.log(id.toFixed(2));     // ❌ Error! string doesn't have toFixed
    
    // But you can use methods that work on both
    console.log(id.toString());        // ✅ OK - both have toString()
    
    // Use type narrowing to access specific methods
    if (typeof id === "string") {
        // TypeScript knows id is a string here
        console.log(id.toUpperCase());  // ✅ OK
    } else {
        // TypeScript knows id is a number here
        console.log(id.toFixed(2));     // ✅ OK
    }
}

printId("abc123");     // Works with string
printId(12345);        // Works with number

🎯 Real-World Example: API Response

APIs often return either data or an error. Union types model this perfectly:

interface SuccessResponse {
    success: true;
    data: any;
}

interface ErrorResponse {
    success: false;
    error: string;
}

// Response is one OR the other
type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
    if (response.success) {
        // TypeScript knows this is SuccessResponse
        console.log("Data:", response.data);
    } else {
        // TypeScript knows this is ErrorResponse
        console.log("Error:", response.error);
    }
}

Arrays with Union Types

You can create arrays that hold multiple types using unions:

// Array that can contain strings OR numbers
let mixed: (string | number)[] = [1, "two", 3, "four"];

// Add items of either type
mixed.push(5);              // ✅ OK
mixed.push("six");          // ✅ OK
mixed.push(true);           // ❌ Error! boolean not in union

// When iterating, you get union type items
mixed.forEach(item => {
    // item is string | number
    console.log(item.toString());  // ✅ Works for both
    
    if (typeof item === "string") {
        console.log(item.toUpperCase());
    }
});
💡 Pro Tip: Use parentheses with array unions: (string | number)[] means "array of string-or-number". Without parentheses, string | number[] means "string or array-of-numbers" - very different!

Union Types in Practice

Here are common patterns you'll use all the time:

// Optional values: value OR null
let userName: string | null = null;
userName = "Alice";          // Can be set later

// Optional values: value OR undefined
let age: number | undefined;
age = 25;

// Multiple choice values
type Size = "small" | "medium" | "large";
let shirtSize: Size = "medium";

// Error or success
type Result = { success: true; value: number } | { success: false; error: string };

function divide(a: number, b: number): Result {
    if (b === 0) {
        return { success: false, error: "Cannot divide by zero" };
    }
    return { success: true, value: a / b };
}

// Using the result
let result = divide(10, 2);
if (result.success) {
    console.log(`Result: ${result.value}`);
} else {
    console.log(`Error: ${result.error}`);
}
graph TD A[Union Type] --> B[Type A] A --> C[Type B] A --> D[Type C] B --> B1[string] C --> C1[number] D --> D1[boolean] E[Value] --> F{Which Type?} F --> B F --> C F --> D style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style F fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff

Literal Types: Be Specific!

Literal types are like ordering a specific item from a menu instead of just saying "food". Instead of saying a variable is a string, you can say it's specifically "red" or "blue" or "green". It's TypeScript's way of saying "not just any value, this EXACT value!" 🎯

String Literal Types

The most common literal types are strings with specific values:

Literal Types: Specific Values Only Comparison showing how string type accepts any string while literal types only accept specific values type: string "anything" ✓ "goes" ✓ "here" ✓ "typo123" ✓ ⚠️ Too permissive! "north" | "south" | "east" | "west" "north" "south" "east" "west" ✅ Only valid values!
// Instead of just 'string', be specific
let direction: "north" | "south" | "east" | "west";

direction = "north";      // ✅ OK
direction = "south";      // ✅ OK
direction = "Northeast";  // ❌ Error! Not one of the allowed values
direction = "NORTH";      // ❌ Error! Case matters!

// Great for status values
let status: "pending" | "approved" | "rejected";
status = "pending";       // ✅ OK
status = "completed";     // ❌ Error! Not in the allowed list

// Traffic light states
let light: "red" | "yellow" | "green";
light = "red";            // ✅ OK
light = "blue";           // ❌ Error! Blue is not a traffic light color!

✅ Why Literal Types Are Awesome

  • Self-documenting: Your code shows exactly what values are valid
  • Autocomplete: Your editor suggests only valid values
  • Typo-proof: Catch misspellings instantly
  • Refactor-safe: Change values confidently

Numeric Literal Types

Numbers can be literal types too! This is perfect for things like HTTP status codes or game scores:

// HTTP status codes
let statusCode: 200 | 404 | 500;

statusCode = 200;     // ✅ OK
statusCode = 404;     // ✅ OK
statusCode = 201;     // ❌ Error! Not in the allowed list

// Dice roll (1-6)
let diceRoll: 1 | 2 | 3 | 4 | 5 | 6;
diceRoll = 4;         // ✅ OK
diceRoll = 7;         // ❌ Error! A die doesn't have 7

// Version numbers
let apiVersion: 1 | 2 | 3;
apiVersion = 2;       // ✅ OK

Boolean Literal Types

Even booleans can be literal types (though this is less common):

// Only true is allowed
let alwaysTrue: true = true;
alwaysTrue = false;    // ❌ Error!

// Only false is allowed
let alwaysFalse: false = false;
alwaysFalse = true;    // ❌ Error!

// This is more useful in discriminated unions (we'll cover this later)
type SuccessResult = { success: true; data: any };
type ErrorResult = { success: false; error: string };

Type Aliases for Literal Types

Instead of repeating literal unions, create reusable type aliases:

// Define once, use everywhere
type Direction = "north" | "south" | "east" | "west";
type Status = "pending" | "approved" | "rejected";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

// Use in multiple places
let heading: Direction = "north";
let orderStatus: Status = "pending";
let requestMethod: HttpMethod = "POST";

// Use in function signatures
function move(direction: Direction): void {
    console.log(`Moving ${direction}`);
}

function updateStatus(newStatus: Status): void {
    console.log(`Status changed to ${newStatus}`);
}

move("north");              // ✅ OK
move("northeast");          // ❌ Error! Typo caught!

updateStatus("approved");   // ✅ OK
updateStatus("complete");   // ❌ Error! Not a valid status!

Combining Literal Types with Other Types

You can mix literal types with regular types for ultimate flexibility:

// A number OR the string "auto"
type Size = number | "auto";

let width: Size = 100;        // ✅ OK - number
let height: Size = "auto";    // ✅ OK - literal string
let depth: Size = "manual";   // ❌ Error! Not allowed

// A specific string OR null
type Theme = "light" | "dark" | null;

let currentTheme: Theme = "dark";    // ✅ OK
currentTheme = null;                 // ✅ OK (user hasn't chosen)
currentTheme = "blue";               // ❌ Error! Not a valid theme

// Numbers in a range OR special values
type Percentage = number | "auto" | "inherit";

let opacity: Percentage = 0.5;       // ✅ OK
let margin: Percentage = "auto";     // ✅ OK
let padding: Percentage = "inherit"; // ✅ OK

🎯 Real-World Example: Button Component

type ButtonVariant = "primary" | "secondary" | "danger" | "success";
type ButtonSize = "small" | "medium" | "large";

interface ButtonProps {
    variant: ButtonVariant;
    size: ButtonSize;
    disabled?: boolean;
    onClick: () => void;
}

function Button(props: ButtonProps) {
    // TypeScript ensures variant and size are valid
    console.log(`Rendering ${props.size} ${props.variant} button`);
}

// Usage
Button({
    variant: "primary",    // ✅ Autocomplete suggests valid values
    size: "large",         // ✅ Autocomplete suggests valid values
    onClick: () => {}
});

// This would error
Button({
    variant: "blue",       // ❌ Error! Not a valid variant
    size: "extra-large",   // ❌ Error! Not a valid size
    onClick: () => {}
});

When to Use Literal Types

Use Case Example Benefit
Fixed set of options Status values, directions, colors Prevents typos and invalid values
API endpoints HTTP methods, status codes Documents valid values
Configuration Environment types, modes Makes config type-safe
UI components Button variants, sizes Autocomplete in editor
State machines State names, events Ensures valid state transitions

Special Types: any, unknown, never, and void

TypeScript has a few special types that handle unique situations. Think of them as the specialized tools in your toolbox - you don't use them every day, but when you need them, they're invaluable! 🛠️

TypeScript Special Types Visual comparison of special types: any (unsafe), unknown (safe), void (no return), and never (impossible) any No type checking Accepts anything ⚠️ AVOID! unknown Must check first Safe alternative ✅ PREFER! void No return value Side effects only Common never Never occurs Throws/infinite Specialized Unsafe Type-safe Choose the right type for your use case!

any: The Escape Hatch (Use Sparingly!)

The any type is like a "get out of jail free" card - it turns off all type checking. It means "I don't care what type this is." While this seems convenient, it defeats the entire purpose of TypeScript! 🚫

// any accepts ANYTHING
let anything: any;

anything = "hello";           // ✅ No error
anything = 42;                // ✅ No error
anything = true;              // ✅ No error
anything = { x: 1 };          // ✅ No error
anything = [1, 2, 3];        // ✅ No error

// You can call any method (even if it doesn't exist!)
anything.toUpperCase();       // ✅ No error at compile time (but crashes at runtime!)
anything.foo.bar.baz();       // ✅ No error at compile time (but crashes at runtime!)

// any spreads through your code like a virus
let x: any = "hello";
let y = x;                    // y is now 'any' too!

❌ Why any is Dangerous

  • No type checking = bugs slip through
  • No autocomplete = slower development
  • No refactoring safety = things break silently
  • Defeats the purpose of TypeScript

Rule of thumb: If you're using any, ask yourself why. There's almost always a better alternative!

unknown: The Safe Alternative to any

unknown is like any's responsible older sibling. It says "I don't know what type this is YET, but I'll check before using it." Much safer! 🛡️

// unknown accepts anything (like any)
let value: unknown;

value = "hello";              // ✅ OK
value = 42;                   // ✅ OK
value = true;                 // ✅ OK

// But you can't use it without checking first!
value.toUpperCase();          // ❌ Error! Must check type first

// You must narrow the type before using it
if (typeof value === "string") {
    // Now TypeScript knows it's a string
    console.log(value.toUpperCase());  // ✅ OK
}

// Type guards work great with unknown
function processValue(value: unknown) {
    // Check what type it is first
    if (typeof value === "string") {
        return value.toUpperCase();
    } else if (typeof value === "number") {
        return value.toFixed(2);
    } else if (Array.isArray(value)) {
        return value.length;
    } else {
        return "Unknown type";
    }
}

✅ When to Use unknown

  • Working with data from external sources (APIs, user input)
  • Deserializing JSON (you don't know what's in it yet)
  • Generic error handling
  • Anytime you'd be tempted to use any - use unknown instead!

void: Functions That Don't Return Anything

void means "this function doesn't return a value" (or returns undefined). It's like a function that does something but doesn't give you anything back. 🤷

// Functions that log, save, or perform side effects
function logMessage(message: string): void {
    console.log(message);
    // No return statement, or returns undefined
}

function saveToDatabase(data: any): void {
    // Save data...
    // Doesn't return anything
}

// void functions can return undefined explicitly
function doNothing(): void {
    return undefined;  // ✅ OK
    // return null;    // ❌ Error! null is not void
    // return 5;       // ❌ Error! number is not void
}

// You can't assign the result to anything meaningful
let result: void = logMessage("Hello");  // result is undefined
let x: number = logMessage("Hello");     // ❌ Error! void is not assignable to number
💡 Fun Fact: In JavaScript, functions without a return statement actually return undefined. In TypeScript, we use void to indicate "this function's return value doesn't matter."

never: The Impossible Type

never represents values that never occur. It's like a function that never returns (because it throws an error or runs forever). Sounds weird, but it's actually useful! 🎭

// Function that always throws an error (never returns)
function throwError(message: string): never {
    throw new Error(message);
    // Execution never reaches here
}

// Function that runs forever (never returns)
function infiniteLoop(): never {
    while (true) {
        // Do something forever
    }
    // Never exits
}

// never is useful for exhaustive checking
type Shape = "circle" | "square" | "triangle";

function getArea(shape: Shape): number {
    switch (shape) {
        case "circle":
            return Math.PI * 10 * 10;
        case "square":
            return 10 * 10;
        case "triangle":
            return 0.5 * 10 * 10;
        default:
            // If we get here, we forgot to handle a case!
            const exhaustiveCheck: never = shape;
            throw new Error(`Unhandled shape: ${exhaustiveCheck}`);
    }
}

// If we add a new shape type and forget to handle it, TypeScript errors!

🎯 When to Use never

  • Error handlers: Functions that always throw
  • Exhaustive checking: Ensure all cases are handled
  • Impossible branches: Code that should never execute
  • Type guards: Eliminate impossible types

Comparing Special Types

Type Meaning Use When Safety Level
any Could be anything (no checking) Migrating from JS (temporarily!) ⚠️ Unsafe - avoid!
unknown Could be anything (must check first) External data, API responses ✅ Safe - recommended
void No return value Functions with side effects ✅ Safe
never Never occurs Error functions, exhaustive checks ✅ Safe

Special Types in Action

Here's how these types work together in a real scenario:

// Fetching data from an API
async function fetchUserData(userId: number): Promise<unknown> {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();  // We don't know what we'll get!
}

// Processing the unknown data safely
function processUserData(data: unknown): string | never {
    // Guard against invalid data
    if (!data || typeof data !== "object") {
        throw new Error("Invalid data");  // Returns never
    }
    
    // Narrow the type
    if ("name" in data && typeof data.name === "string") {
        return data.name;  // Returns string
    }
    
    throw new Error("Data missing name property");  // Returns never
}

// A function that logs (returns void)
function logUser(name: string): void {
    console.log(`User: ${name}`);
}

// Using them together
async function displayUser(userId: number): Promise<void> {
    try {
        const data = await fetchUserData(userId);  // unknown
        const name = processUserData(data);        // string or never
        logUser(name);                             // void
    } catch (error) {
        console.error("Failed to display user");
    }
}
graph TD A[Special Types] --> B[any] A --> C[unknown] A --> D[void] A --> E[never] B --> B1["No type checking
AVOID!"] C --> C1["Type checking required
SAFE"] D --> D1["No return value
Common"] E --> E1["Never returns
Specialized"] style B fill:#f44336,stroke:#333,stroke-width:2px,color:#fff style C fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff style D fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff style E fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff
🎯 Remember: Prefer unknown over any, use void for functions without returns, and leverage never for error handling and exhaustive checks. Each has its place, but they're specialized tools - use them wisely!

Hands-on Practice: Build a Type-Safe Inventory System

Time to put everything together! We're going to build a complete inventory management system that uses all the types we've learned. This is a real-world scenario - imagine you're building software for a warehouse. Let's make it type-safe and bulletproof! 📦

🎯 Project Goal

Create an inventory system that tracks products, manages stock, and processes orders using TypeScript's type system to prevent errors.

Step 1: Define Your Types

First, let's define the structure of our data using the types we've learned:

// Product categories as literal types
type Category = "electronics" | "clothing" | "food" | "books";

// Product status
type ProductStatus = "in-stock" | "low-stock" | "out-of-stock";

// ID can be either string or number
type ProductId = string | number;

// A complete product interface
interface Product {
    id: ProductId;
    name: string;
    category: Category;
    price: number;
    quantity: number;
    status: ProductStatus;
    tags: string[];
    supplier?: string;  // Optional - not all products have suppliers
}

// Order result types
type OrderSuccess = {
    success: true;
    orderId: string;
    total: number;
};

type OrderError = {
    success: false;
    error: string;
};

type OrderResult = OrderSuccess | OrderError;

Step 2: Create the Inventory Class

Now let's build the actual inventory system with type-safe methods:

class Inventory {
    private products: Product[] = [];
    
    // Add a product to inventory
    addProduct(product: Product): void {
        // Check if product already exists
        const exists = this.products.some(p => p.id === product.id);
        
        if (exists) {
            console.log(`Product ${product.id} already exists`);
            return;
        }
        
        this.products.push(product);
        console.log(`Added ${product.name} to inventory`);
    }
    
    // Find a product by ID
    findProduct(id: ProductId): Product | undefined {
        return this.products.find(p => p.id === id);
    }
    
    // Get all products in a category
    getByCategory(category: Category): Product[] {
        return this.products.filter(p => p.category === category);
    }
    
    // Update product status based on quantity
    updateStatus(id: ProductId): void {
        const product = this.findProduct(id);
        
        if (!product) {
            console.log("Product not found");
            return;
        }
        
        // Determine status based on quantity
        if (product.quantity === 0) {
            product.status = "out-of-stock";
        } else if (product.quantity < 10) {
            product.status = "low-stock";
        } else {
            product.status = "in-stock";
        }
    }
    
    // Process an order
    processOrder(id: ProductId, quantity: number): OrderResult {
        const product = this.findProduct(id);
        
        // Validate product exists
        if (!product) {
            return {
                success: false,
                error: "Product not found"
            };
        }
        
        // Validate quantity
        if (quantity <= 0) {
            return {
                success: false,
                error: "Invalid quantity"
            };
        }
        
        // Check stock
        if (product.quantity < quantity) {
            return {
                success: false,
                error: `Only ${product.quantity} units available`
            };
        }
        
        // Process the order
        product.quantity -= quantity;
        this.updateStatus(id);
        
        const total = product.price * quantity;
        const orderId = `ORD-${Date.now()}`;
        
        return {
            success: true,
            orderId,
            total
        };
    }
    
    // Get inventory summary
    getSummary(): string {
        const total = this.products.length;
        const inStock = this.products.filter(p => p.status === "in-stock").length;
        const lowStock = this.products.filter(p => p.status === "low-stock").length;
        const outOfStock = this.products.filter(p => p.status === "out-of-stock").length;
        
        return `Inventory Summary:
Total Products: ${total}
In Stock: ${inStock}
Low Stock: ${lowStock}
Out of Stock: ${outOfStock}`;
    }
}

Step 3: Use the Inventory System

Let's test our type-safe inventory system:

// Create an inventory
const inventory = new Inventory();

// Add products with full type safety
inventory.addProduct({
    id: 1,
    name: "Laptop",
    category: "electronics",
    price: 999.99,
    quantity: 15,
    status: "in-stock",
    tags: ["computer", "portable", "work"],
    supplier: "TechCorp"
});

inventory.addProduct({
    id: "BOOK-001",
    name: "TypeScript Handbook",
    category: "books",
    price: 39.99,
    quantity: 8,
    status: "low-stock",
    tags: ["programming", "education"]
    // No supplier - it's optional!
});

inventory.addProduct({
    id: 2,
    name: "T-Shirt",
    category: "clothing",
    price: 19.99,
    quantity: 0,
    status: "out-of-stock",
    tags: ["casual", "cotton"]
});

// Try to add invalid product (TypeScript catches these!)
/*
inventory.addProduct({
    id: 3,
    name: "Widget",
    category: "gadgets",  // ❌ Error! "gadgets" is not a valid category
    price: "29.99",       // ❌ Error! Price must be a number
    quantity: 5,
    status: "available",  // ❌ Error! "available" is not a valid status
    tags: "tag1, tag2"    // ❌ Error! tags must be an array
});
*/

// Find products
const laptop = inventory.findProduct(1);
console.log(laptop?.name);  // "Laptop"

// Get products by category
const electronics = inventory.getByCategory("electronics");
console.log(`Electronics: ${electronics.length} items`);

// Process orders
const order1 = inventory.processOrder(1, 3);

if (order1.success) {
    // TypeScript knows this has orderId and total
    console.log(`Order placed! ID: ${order1.orderId}, Total: $${order1.total}`);
} else {
    // TypeScript knows this has error
    console.log(`Order failed: ${order1.error}`);
}

// Try to order more than available
const order2 = inventory.processOrder("BOOK-001", 20);

if (order2.success) {
    console.log(`Success!`);
} else {
    console.log(`Error: ${order2.error}`);  // "Only 8 units available"
}

// Get summary
console.log(inventory.getSummary());

✅ Output:

Added Laptop to inventory
Added TypeScript Handbook to inventory
Added T-Shirt to inventory
Laptop
Electronics: 1 items
Order placed! ID: ORD-1234567890, Total: $2999.97
Error: Only 8 units available
Inventory Summary:
Total Products: 3
In Stock: 1
Low Stock: 1
Out of Stock: 1

Your Turn: Extend the System

🏋️ Exercise 1: Add Discount Functionality

Challenge: Add a method to apply discounts to products. Create a Discount type that can be either a percentage (number) or a fixed amount ("fixed"), and implement an applyDiscount method.

💡 Hint

Think about using a discriminated union for the discount type:

type PercentDiscount = { type: "percent"; value: number };
type FixedDiscount = { type: "fixed"; amount: number };
type Discount = PercentDiscount | FixedDiscount;
✅ Solution
type PercentDiscount = { type: "percent"; value: number };
type FixedDiscount = { type: "fixed"; amount: number };
type Discount = PercentDiscount | FixedDiscount;

class Inventory {
    // ... previous methods ...
    
    applyDiscount(id: ProductId, discount: Discount): void {
        const product = this.findProduct(id);
        
        if (!product) {
            console.log("Product not found");
            return;
        }
        
        if (discount.type === "percent") {
            // TypeScript knows discount.value exists
            product.price = product.price * (1 - discount.value / 100);
        } else {
            // TypeScript knows discount.amount exists
            product.price = Math.max(0, product.price - discount.amount);
        }
        
        console.log(`Discount applied. New price: $${product.price.toFixed(2)}`);
    }
}

// Usage
inventory.applyDiscount(1, { type: "percent", value: 10 });  // 10% off
inventory.applyDiscount(2, { type: "fixed", amount: 5 });    // $5 off

🏋️ Exercise 2: Add Search Functionality

Challenge: Create a searchProducts method that can search by name (string), category (Category), or price range (tuple of [min, max]).

💡 Hint

Use union types and type guards to handle different search criteria:

type SearchCriteria = 
    | { type: "name"; value: string }
    | { type: "category"; value: Category }
    | { type: "priceRange"; min: number; max: number };
✅ Solution
type SearchCriteria = 
    | { type: "name"; value: string }
    | { type: "category"; value: Category }
    | { type: "priceRange"; min: number; max: number };

class Inventory {
    // ... previous methods ...
    
    searchProducts(criteria: SearchCriteria): Product[] {
        switch (criteria.type) {
            case "name":
                return this.products.filter(p => 
                    p.name.toLowerCase().includes(criteria.value.toLowerCase())
                );
            
            case "category":
                return this.products.filter(p => 
                    p.category === criteria.value
                );
            
            case "priceRange":
                return this.products.filter(p => 
                    p.price >= criteria.min && p.price <= criteria.max
                );
            
            default:
                const exhaustiveCheck: never = criteria;
                throw new Error(`Unhandled criteria: ${exhaustiveCheck}`);
        }
    }
}

// Usage
const byName = inventory.searchProducts({ type: "name", value: "shirt" });
const byCategory = inventory.searchProducts({ type: "category", value: "electronics" });
const byPrice = inventory.searchProducts({ type: "priceRange", min: 10, max: 50 });

🎉 Amazing Work!

You just built a complete, type-safe inventory system using primitive types, arrays, tuples, unions, literals, and special types. This is the foundation of real-world TypeScript development! 🚀

Best Practices for Using Basic Types

You now know all the basic types! Let's make sure you use them effectively. Here are the golden rules that will make your TypeScript code shine! ✨

✅ Do's: Good Type Habits

1. Use the Most Specific Type Possible

// ❌ Too general
let status: string = "pending";

// ✅ Specific and safe
let status: "pending" | "approved" | "rejected" = "pending";

2. Leverage Type Inference

// ❌ Redundant type annotation
let count: number = 0;
let message: string = "Hello";

// ✅ Let TypeScript infer
let count = 0;              // Inferred as number
let message = "Hello";      // Inferred as string

3. Use Readonly for Tuples When Appropriate

// ❌ Tuple can be modified
let point: [number, number] = [10, 20];
point.push(30);  // Breaks the tuple!

// ✅ Readonly tuple is safer
let point: readonly [number, number] = [10, 20];
// point.push(30);  // ❌ Error! Can't modify

4. Prefer unknown Over any

// ❌ Unsafe
function process(data: any) {
    return data.value.toUpperCase();  // Runtime error if data is wrong!
}

// ✅ Safe
function process(data: unknown) {
    if (data && typeof data === "object" && "value" in data) {
        const obj = data as { value: unknown };
        if (typeof obj.value === "string") {
            return obj.value.toUpperCase();  // Safe!
        }
    }
    return "Invalid data";
}

5. Use Type Aliases for Complex Types

// ❌ Repetitive and hard to maintain
function processUser(user: { id: number; name: string; email: string }): void {}
function updateUser(user: { id: number; name: string; email: string }): void {}

// ✅ Define once, use everywhere
type User = {
    id: number;
    name: string;
    email: string;
};

function processUser(user: User): void {}
function updateUser(user: User): void {}

❌ Don'ts: Common Mistakes to Avoid

1. Don't Use any Unless Absolutely Necessary

// ❌ Defeats the purpose of TypeScript
let data: any = fetchData();
data.anything.can.happen();  // No type safety!

// ✅ Use proper types or unknown
let data: User | null = fetchData();
if (data) {
    console.log(data.name);  // Type-safe!
}

2. Don't Forget Array Type Annotations for Empty Arrays

// ❌ TypeScript doesn't know what type will go in here
let items = [];
items.push("string");
items.push(123);  // No error, but probably a bug!

// ✅ Specify the type
let items: string[] = [];
items.push("string");  // ✅ OK
items.push(123);       // ❌ Error!

3. Don't Use String When You Mean Literal Types

// ❌ Too permissive
function setTheme(theme: string) {
    // What if someone passes "purple"?
}

// ✅ Restrict to valid values
function setTheme(theme: "light" | "dark") {
    // Only valid themes allowed!
}

4. Don't Mix Up Tuples and Arrays

// ❌ Wrong: This is an array, not a tuple
let coordinates: number[] = [10, 20];
coordinates.push(30);  // Allowed, but breaks the coordinate concept

// ✅ Right: Use tuple for fixed structure
let coordinates: [number, number] = [10, 20];
// coordinates.push(30);  // Would be an error with readonly

💡 Pro Tips

Tip 1: Use Union Types for Flexible APIs

When a function can accept multiple types, union types make it type-safe:

function formatId(id: string | number): string {
    return `ID-${id}`;
}

formatId(123);      // ✅ Works
formatId("abc");    // ✅ Works
formatId(true);     // ❌ Error!

Tip 2: Combine Literal Types with Union Types

This pattern is incredibly powerful for building type-safe APIs:

type Result<T> = 
    | { success: true; data: T }
    | { success: false; error: string };

function handleResult<T>(result: Result<T>): void {
    if (result.success) {
        console.log(result.data);   // TypeScript knows data exists
    } else {
        console.log(result.error);  // TypeScript knows error exists
    }
}

Tip 3: Document Your Types

Use JSDoc comments to explain non-obvious type choices:

/**
 * User status in the system
 * - pending: Awaiting email verification
 * - active: Fully verified and active
 * - suspended: Temporarily blocked
 */
type UserStatus = "pending" | "active" | "suspended";

Quick Reference Card

Type Use For Example
string Text data let name: string = "Alice"
number Numeric data let age: number = 25
boolean True/false values let active: boolean = true
Type[] Arrays of same type let nums: number[] = [1, 2, 3]
[Type, Type] Fixed-length mixed types let point: [number, number] = [x, y]
Type | Type One of several types let id: string | number
"literal" Exact value only let status: "pending" | "done"
unknown Unknown data (check first) let data: unknown = getData()
void No return value function log(): void {}
never Never returns function fail(): never { throw }

Summary

🎉 Key Takeaways

  • Primitive types (string, number, boolean) are the foundation of TypeScript
  • Arrays hold collections of the same type: Type[]
  • Tuples represent fixed-length arrays with specific types at each position
  • Type inference lets TypeScript figure out types automatically - use it!
  • Union types (Type | Type) allow values to be one of several types
  • Literal types restrict values to specific strings, numbers, or booleans
  • unknown is the safe alternative to any - always prefer it
  • void indicates functions that don't return values
  • never represents values that never occur (errors, infinite loops)
  • Avoid any at all costs - it defeats TypeScript's purpose

The Type Hierarchy

graph TB A[TypeScript Types] --> B[Primitive Types] A --> C[Collection Types] A --> D[Special Types] B --> B1[string] B --> B2[number] B --> B3[boolean] C --> C1[Arrays] C --> C2[Tuples] D --> D1[any - avoid!] D --> D2[unknown - prefer] D --> D3[void] D --> D4[never] E[Type Modifiers] --> E1[Union: A | B] E --> E2["Literal: 'exact'"] E --> E3[Optional: Type?] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style D1 fill:#f44336,stroke:#333,stroke-width:2px,color:#fff style D2 fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff

What You've Mastered

Congratulations! You now have a solid grasp of TypeScript's type system. You can:

  • ✅ Use primitive types confidently in any scenario
  • ✅ Work with arrays and understand when to use tuples
  • ✅ Create flexible APIs with union types
  • ✅ Restrict values with literal types
  • ✅ Handle unknown data safely with type guards
  • ✅ Build complete, type-safe applications

Real-World Impact

The types you learned today prevent real bugs in production:

🐛 Bugs You'll Prevent

  • Type mismatches: No more "undefined is not a function" errors
  • Invalid values: Status can't be "complted" (typo) anymore
  • Wrong array operations: Can't add numbers to a string array
  • Missing properties: TypeScript catches them immediately
  • Incorrect function calls: Wrong number or type of arguments caught instantly

📚 Additional Resources

🚀 What's Next?

In the next lesson, we'll explore Interfaces and Type Aliases. You'll learn how to:

  • Define complex object shapes with interfaces
  • Understand when to use interfaces vs type aliases
  • Extend and compose types
  • Create index signatures for dynamic properties
  • Use optional and readonly properties effectively

This is where we start building the complex type structures you see in real applications! 🏗️

Quick Knowledge Check

🎯 Test Your Understanding

Question 1: What's the difference between string[] and [string, string]?

Question 2: When should you use unknown instead of any?

Question 3: What's better: let status: string or let status: "pending" | "active" | "done"?

🎉 Outstanding Work!

You've mastered TypeScript's basic types! You now have the foundation to build type-safe applications. The types you learned today will be used in every single TypeScript project you work on.

Keep practicing, and get ready to level up with interfaces and type aliases! 🚀