🎨 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.
// 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 |
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?
| 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] |
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:
// 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:
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}`);
}
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:
// 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! 🛠️
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- useunknowninstead!
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 returnundefined. In TypeScript, we usevoidto 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");
}
}
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: Preferunknownoverany, usevoidfor functions without returns, and leverageneverfor 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
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
- TypeScript Handbook: Everyday Types
- TypeScript Handbook: Narrowing
- TypeScript Playground - Practice what you learned!
- Type Challenges - Level up your skills
🚀 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! 🚀