Prerequisites
- Basic TypeScript types knowledge ๐
- Understanding of interfaces โก
- Functions basics ๐ป
What you'll learn
- Create and use type aliases effectively ๐ฏ
- Master union and intersection types ๐๏ธ
- Build complex types with utility types ๐
- Apply type aliases in real projects โจ
๐ฏ Introduction
Welcome to the world of TypeScript type aliases! ๐ If interfaces are like blueprints for objects, type aliases are like custom labels you can stick on any type - from simple primitives to complex structures.
Think of type aliases as nicknames for types ๐ท๏ธ. Just like you might call your friend โAlexโ instead of โAlexander Christopher Thompson IIIโ, type aliases let you give friendly names to complex types. They make your code more readable, maintainable, and DRY (Donโt Repeat Yourself)!
By the end of this tutorial, youโll be creating elegant type aliases that make your code a joy to read and work with! Letโs dive in! ๐โโ๏ธ
๐ Understanding Type Aliases
๐ค What Are Type Aliases?
Type aliases create new names for existing types. Theyโre like creating a shortcut or bookmark ๐ that points to any type - primitives, objects, unions, functions, or even other aliases!
Unlike interfaces (which only describe object shapes), type aliases can represent:
- โจ Primitive types (string, number, boolean)
- ๐ Union types (this OR that)
- ๐ก๏ธ Intersection types (this AND that)
- ๐ Function types
- ๐จ Literal types
- ๐ง Any complex type combination!
๐ก Type Alias vs Interface
When should you use each?
// ๐ฏ Type alias - flexible, works with any type
type UserID = string | number;
type Point = { x: number; y: number };
type Callback = (data: string) => void;
// ๐ Interface - best for object shapes
interface User {
id: UserID; // Can use type alias!
name: string;
location: Point;
}
// ๐ก Key differences:
// 1. Type aliases can represent primitives and unions
type Status = "active" | "inactive"; // Can't do with interface!
// 2. Interfaces can be extended/implemented
class Employee implements User {
id = "EMP001";
name = "Alice";
location = { x: 10, y: 20 };
}
// 3. Type aliases can't be merged (interfaces can)
interface Window {
customProperty: string; // Merges with global Window
}
๐ง Basic Type Aliases
๐ Creating Simple Aliases
Letโs start with the basics:
// ๐จ Primitive type aliases
type Username = string;
type Age = number;
type IsActive = boolean;
// โ
Using type aliases
let currentUser: Username = "TypeScriptNinja";
let userAge: Age = 25;
let isOnline: IsActive = true;
// ๐ ID type that could be string or number
type ID = string | number;
function getUser(id: ID): void {
console.log(`Fetching user with ID: ${id}`);
}
getUser("USER-123"); // โ
String ID
getUser(456); // โ
Numeric ID
// ๐ Object type aliases
type Coordinates = {
x: number;
y: number;
z?: number; // Optional for 2D/3D
};
const position2D: Coordinates = { x: 10, y: 20 };
const position3D: Coordinates = { x: 10, y: 20, z: 30 };
// ๐ฎ Function type aliases
type ClickHandler = (event: MouseEvent) => void;
type AsyncCallback<T> = (data: T) => Promise<void>;
const handleClick: ClickHandler = (event) => {
console.log(`Clicked at ${event.clientX}, ${event.clientY}`);
};
๐จ Literal Type Aliases
Create types from exact values:
// ๐ฏ String literal types
type Direction = "north" | "south" | "east" | "west";
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
// ๐ข Numeric literal types
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
type PowerOfTwo = 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128;
// ๐ Mixed literal types
type GameInput = "UP" | "DOWN" | "LEFT" | "RIGHT" | 32 | 13; // Arrow keys or space/enter
// ๐ฎ Using literal types
function move(direction: Direction): void {
switch (direction) {
case "north":
console.log("Moving up! โฌ๏ธ");
break;
case "south":
console.log("Moving down! โฌ๏ธ");
break;
case "east":
console.log("Moving right! โก๏ธ");
break;
case "west":
console.log("Moving left! โฌ
๏ธ");
break;
}
}
// ๐ฒ Dice game example
function rollDice(): DiceRoll {
return Math.floor(Math.random() * 6 + 1) as DiceRoll;
}
const roll = rollDice();
console.log(`You rolled a ${roll}! ๐ฒ`);
๐ก Union Types
๐ Creating Union Types
Union types represent values that can be one of several types:
// ๐ฏ Basic union types
type StringOrNumber = string | number;
type NullableString = string | null;
type BooleanOrUndefined = boolean | undefined;
// ๐ Real-world example: API responses
type APIResponse<T> =
| { success: true; data: T }
| { success: false; error: string };
// Using the API response type
function handleResponse(response: APIResponse<User>): void {
if (response.success) {
console.log(`Welcome, ${response.data.name}! โ
`);
} else {
console.log(`Error: ${response.error} โ`);
}
}
// ๐จ Theme system with unions
type ColorScheme = "light" | "dark" | "auto";
type ThemeColor = "primary" | "secondary" | "accent" | "error" | "warning" | "success";
type Theme = {
scheme: ColorScheme;
colors: Record<ThemeColor, string>;
};
// ๐ฎ Game state unions
type GameState =
| { status: "menu" }
| { status: "playing"; level: number; score: number }
| { status: "paused"; level: number; score: number }
| { status: "gameOver"; finalScore: number; highScore: number };
function renderGame(state: GameState): void {
switch (state.status) {
case "menu":
console.log("๐ฎ Main Menu");
break;
case "playing":
console.log(`๐ Level ${state.level} - Score: ${state.score}`);
break;
case "paused":
console.log(`โธ๏ธ Paused at Level ${state.level}`);
break;
case "gameOver":
console.log(`๐ Game Over! Score: ${state.finalScore}`);
if (state.finalScore > state.highScore) {
console.log("๐ New High Score!");
}
break;
}
}
๐ก๏ธ Discriminated Unions
Use a common property to distinguish between union members:
// ๐ Vehicle type system
type Car = {
type: "car";
brand: string;
doors: number;
fuelType: "gas" | "electric" | "hybrid";
};
type Motorcycle = {
type: "motorcycle";
brand: string;
engineSize: number; // in CC
};
type Bicycle = {
type: "bicycle";
brand: string;
gears: number;
isElectric: boolean;
};
type Vehicle = Car | Motorcycle | Bicycle;
// ๐ฏ Type-safe vehicle handling
function describeVehicle(vehicle: Vehicle): string {
switch (vehicle.type) {
case "car":
return `๐ ${vehicle.brand} car with ${vehicle.doors} doors (${vehicle.fuelType})`;
case "motorcycle":
return `๐๏ธ ${vehicle.brand} motorcycle with ${vehicle.engineSize}cc engine`;
case "bicycle":
return `๐ฒ ${vehicle.brand} bicycle with ${vehicle.gears} gears${vehicle.isElectric ? " (electric)" : ""}`;
}
}
// ๐ฑ Notification system
type NotificationBase = {
id: string;
timestamp: Date;
read: boolean;
};
type MessageNotification = NotificationBase & {
type: "message";
from: string;
preview: string;
};
type FriendRequestNotification = NotificationBase & {
type: "friend_request";
from: string;
mutualFriends: number;
};
type SystemNotification = NotificationBase & {
type: "system";
title: string;
message: string;
severity: "info" | "warning" | "error";
};
type Notification = MessageNotification | FriendRequestNotification | SystemNotification;
function renderNotification(notification: Notification): void {
const baseInfo = `[${notification.timestamp.toLocaleTimeString()}]`;
switch (notification.type) {
case "message":
console.log(`๐ฌ ${baseInfo} New message from ${notification.from}: "${notification.preview}"`);
break;
case "friend_request":
console.log(`๐ฅ ${baseInfo} Friend request from ${notification.from} (${notification.mutualFriends} mutual friends)`);
break;
case "system":
const icon = notification.severity === "error" ? "๐จ" : notification.severity === "warning" ? "โ ๏ธ" : "โน๏ธ";
console.log(`${icon} ${baseInfo} ${notification.title}: ${notification.message}`);
break;
}
}
๐ Intersection Types
๐ Combining Types
Intersection types combine multiple types into one:
// ๐ฏ Basic intersection
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged; // Has both name AND age
const person: Person = {
name: "Sarah",
age: 28
};
// ๐ข Employee system with intersections
type Employee = {
employeeId: string;
department: string;
salary: number;
};
type Manager = {
teamSize: number;
budget: number;
};
type Developer = {
programmingLanguages: string[];
githubUsername: string;
};
// Combining roles
type TechLead = Employee & Developer & Manager;
const techLead: TechLead = {
// From Employee
employeeId: "EMP001",
department: "Engineering",
salary: 120000,
// From Developer
programmingLanguages: ["TypeScript", "Python", "Go"],
githubUsername: "awesome-dev",
// From Manager
teamSize: 5,
budget: 500000
};
// ๐จ Mixin pattern with intersections
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
type Identifiable = {
id: string;
};
type Versionable = {
version: number;
};
// Create rich domain objects
type Document = Identifiable & Timestamped & Versionable & {
title: string;
content: string;
author: string;
};
type Comment = Identifiable & Timestamped & {
text: string;
authorId: string;
documentId: string;
};
// ๐ก๏ธ Permission system
type ReadPermission = { canRead: true };
type WritePermission = { canWrite: true };
type DeletePermission = { canDelete: true };
type AdminPermission = { isAdmin: true };
type ReadOnlyUser = ReadPermission;
type StandardUser = ReadPermission & WritePermission;
type ModeratorUser = ReadPermission & WritePermission & DeletePermission;
type SuperUser = ReadPermission & WritePermission & DeletePermission & AdminPermission;
function checkPermissions(user: StandardUser): void {
if (user.canRead) console.log("โ
Can read");
if (user.canWrite) console.log("โ
Can write");
// if (user.canDelete) console.log("Can delete"); // Error! StandardUser doesn't have canDelete
}
๐จ Advanced Type Aliases
๐งฎ Generic Type Aliases
Make your types reusable with generics:
// ๐ฏ Generic wrapper types
type Nullable<T> = T | null;
type Optional<T> = T | undefined;
type AsyncData<T> = Promise<T>;
// Usage
type NullableUser = Nullable<User>; // User | null
type OptionalString = Optional<string>; // string | undefined
// ๐ฆ Result type for error handling
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return { ok: false, error: "Division by zero!" };
}
return { ok: true, value: a / b };
}
const result = divide(10, 2);
if (result.ok) {
console.log(`Result: ${result.value}`); // 5
} else {
console.log(`Error: ${result.error}`);
}
// ๐ State management types
type LoadingState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
// ๐ฎ Game inventory system
type InventorySlot<T> = {
item: T | null;
quantity: number;
maxStack: number;
};
type Weapon = { type: "weapon"; damage: number; durability: number };
type Armor = { type: "armor"; defense: number; durability: number };
type Consumable = { type: "consumable"; effect: string; uses: number };
type GameItem = Weapon | Armor | Consumable;
type Inventory = InventorySlot<GameItem>[];
// ๐ณ Tree structure
type TreeNode<T> = {
value: T;
children: TreeNode<T>[];
};
const fileSystem: TreeNode<string> = {
value: "root",
children: [
{
value: "src",
children: [
{ value: "index.ts", children: [] },
{ value: "types.ts", children: [] }
]
},
{
value: "tests",
children: [
{ value: "index.test.ts", children: [] }
]
}
]
};
๐ ๏ธ Utility Type Aliases
Create powerful utility types:
// ๐ฏ Object key extraction
type ObjectKeys<T> = keyof T;
type ObjectValues<T> = T[keyof T];
interface Product {
id: string;
name: string;
price: number;
}
type ProductKeys = ObjectKeys<Product>; // "id" | "name" | "price"
type ProductValues = ObjectValues<Product>; // string | number
// ๐ง Deep readonly type
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
type Config = {
api: {
url: string;
key: string;
};
features: {
darkMode: boolean;
analytics: boolean;
};
};
type ImmutableConfig = DeepReadonly<Config>;
// Now config.api.url is readonly too!
// ๐จ Conditional types
type IsArray<T> = T extends any[] ? true : false;
type ElementType<T> = T extends (infer E)[] ? E : never;
type Test1 = IsArray<string[]>; // true
type Test2 = IsArray<string>; // false
type Test3 = ElementType<number[]>; // number
type Test4 = ElementType<string>; // never
// ๐๏ธ Builder pattern types
type Builder<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => Builder<T>;
} & {
build(): T;
};
// ๐ Form field types
type FormField<T> = {
value: T;
error?: string;
touched: boolean;
dirty: boolean;
};
type FormValues<T> = {
[K in keyof T]: FormField<T[K]>;
};
interface LoginForm {
username: string;
password: string;
rememberMe: boolean;
}
type LoginFormState = FormValues<LoginForm>;
// Creates form state with FormField for each property!
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Type Alias Recursion
// โ Direct recursion causes error
// type RecursiveArray<T> = T | RecursiveArray<T>[];
// Error: Type alias 'RecursiveArray' circularly references itself
// โ
Solution: Use interface for recursion
interface RecursiveArray<T> {
value: T | RecursiveArray<T>[];
}
// Or use intermediate type
type NestedArray<T> = T | { items: NestedArray<T>[] };
๐คฏ Pitfall 2: Union Type Exhaustiveness
type Status = "pending" | "approved" | "rejected";
// โ Forgetting to handle all cases
function handleStatus(status: Status) {
switch (status) {
case "pending":
return "Waiting...";
case "approved":
return "Success!";
// Oops! Forgot "rejected"
}
}
// โ
Exhaustive check
function handleStatusSafely(status: Status): string {
switch (status) {
case "pending":
return "Waiting... โณ";
case "approved":
return "Success! โ
";
case "rejected":
return "Rejected โ";
default:
// This ensures we handle all cases
const _exhaustive: never = status;
return _exhaustive;
}
}
๐ต Pitfall 3: Type Alias vs Interface Confusion
// โ Using type alias when interface is better
type UserType = {
name: string;
age: number;
};
// Can't extend with 'extends' keyword
// type AdminType extends UserType { } // Error!
// โ
Use interface for object types that need extension
interface User {
name: string;
age: number;
}
interface Admin extends User {
permissions: string[];
}
// โ
Use type alias for unions, primitives, etc.
type ID = string | number;
type Status = "active" | "inactive";
๐ ๏ธ Best Practices
๐ฏ Type Alias Best Practices
-
๐ Descriptive Names: Make intent clear
// โ Vague type Data = string | number; // โ Descriptive type UserIdentifier = string | number;
-
๐๏ธ Single Responsibility: One concept per type
// โ Too many concerns type UserDataAndStatus = { name: string; email: string; isActive: boolean; lastLogin: Date; }; // โ Separate concerns type UserData = { name: string; email: string }; type UserStatus = { isActive: boolean; lastLogin: Date }; type User = UserData & UserStatus;
-
๐จ Use Union Types for States: Model mutually exclusive states
// โ Clear state representation type RequestState<T> = | { type: "idle" } | { type: "loading" } | { type: "success"; data: T } | { type: "error"; message: string };
-
โจ Prefer Type Aliases for: Unions, intersections, and utilities
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"; type Coordinates = [x: number, y: number]; type AsyncCallback<T> = (data: T) => Promise<void>;
-
๐ง Create Utility Types: Build reusable type utilities
type Nullable<T> = T | null; type Keys<T> = keyof T; type Values<T> = T[keyof T]; type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Type-Safe Event System
Create a flexible event system using type aliases:
๐ Requirements:
- โ Define various event types (click, input, custom)
- ๐ท๏ธ Create type-safe event handlers
- ๐ฅ Support event listeners with proper typing
- ๐ Add event metadata (timestamp, source)
- ๐จ Handle different payload types
๐ Bonus Points:
- Add event filtering by type
- Create event history tracking
- Implement type-safe event emitter
๐ก Solution
๐ Click to see solution
// ๐ฏ Event type definitions
type EventMetadata = {
timestamp: Date;
source: string;
userId?: string;
};
// Base event structure
type BaseEvent<T extends string, P = void> = EventMetadata & {
type: T;
payload: P;
};
// ๐ฎ Specific event types
type ClickEvent = BaseEvent<"click", {
x: number;
y: number;
target: string;
}>;
type InputEvent = BaseEvent<"input", {
field: string;
value: string;
previousValue: string;
}>;
type NavigationEvent = BaseEvent<"navigation", {
from: string;
to: string;
method: "push" | "replace" | "back";
}>;
type UserEvent = BaseEvent<"user", {
action: "login" | "logout" | "register";
email: string;
}>;
type CustomEvent<T extends string, P> = BaseEvent<T, P>;
// ๐ Union of all events
type AppEvent = ClickEvent | InputEvent | NavigationEvent | UserEvent;
// ๐ฏ Event handler types
type EventHandler<T extends AppEvent> = (event: T) => void | Promise<void>;
type UnsubscribeFn = () => void;
// ๐ Event listener map
type EventListenerMap = {
[K in AppEvent["type"]]?: EventHandler<Extract<AppEvent, { type: K }>>[];
};
// ๐๏ธ Event emitter class
class TypedEventEmitter {
private listeners: EventListenerMap = {};
private eventHistory: AppEvent[] = [];
// ๐ Subscribe to events
on<T extends AppEvent["type"]>(
eventType: T,
handler: EventHandler<Extract<AppEvent, { type: T }>>
): UnsubscribeFn {
if (!this.listeners[eventType]) {
this.listeners[eventType] = [];
}
this.listeners[eventType]!.push(handler as any);
// Return unsubscribe function
return () => {
const handlers = this.listeners[eventType];
if (handlers) {
const index = handlers.indexOf(handler as any);
if (index > -1) {
handlers.splice(index, 1);
}
}
};
}
// ๐ Emit events
emit<T extends AppEvent>(event: T): void {
// Add to history
this.eventHistory.push(event);
// Get handlers for this event type
const handlers = this.listeners[event.type];
if (handlers) {
handlers.forEach(handler => {
try {
(handler as EventHandler<T>)(event);
} catch (error) {
console.error(`Error in ${event.type} handler:`, error);
}
});
}
}
// ๐ Get filtered history
getHistory<T extends AppEvent["type"]>(
eventType?: T
): T extends undefined ? AppEvent[] : Extract<AppEvent, { type: T }>[] {
if (!eventType) {
return this.eventHistory as any;
}
return this.eventHistory.filter(
event => event.type === eventType
) as any;
}
// ๐ Get event statistics
getStats(): Record<AppEvent["type"], number> {
const stats: Partial<Record<AppEvent["type"], number>> = {};
this.eventHistory.forEach(event => {
stats[event.type] = (stats[event.type] || 0) + 1;
});
return stats as Record<AppEvent["type"], number>;
}
}
// ๐ฎ Usage example
const eventEmitter = new TypedEventEmitter();
// Subscribe to click events
const unsubscribeClick = eventEmitter.on("click", (event) => {
console.log(`๐ฑ๏ธ Clicked at (${event.payload.x}, ${event.payload.y}) on ${event.payload.target}`);
});
// Subscribe to navigation events
eventEmitter.on("navigation", (event) => {
console.log(`๐งญ Navigating from ${event.payload.from} to ${event.payload.to}`);
});
// Subscribe to user events
eventEmitter.on("user", async (event) => {
console.log(`๐ค User ${event.payload.action}: ${event.payload.email}`);
if (event.payload.action === "login") {
// Emit navigation after login
eventEmitter.emit({
type: "navigation",
payload: { from: "/login", to: "/dashboard", method: "replace" },
timestamp: new Date(),
source: "auth"
});
}
});
// ๐ Emit events
eventEmitter.emit({
type: "click",
payload: { x: 100, y: 200, target: "button#submit" },
timestamp: new Date(),
source: "ui"
});
eventEmitter.emit({
type: "user",
payload: { action: "login", email: "[email protected]" },
timestamp: new Date(),
source: "auth",
userId: "USER-123"
});
// ๐ Check statistics
console.log("Event Statistics:", eventEmitter.getStats());
// ๐ Get specific event history
const clickHistory = eventEmitter.getHistory("click");
console.log(`Total clicks: ${clickHistory.length}`);
// ๐งน Cleanup
unsubscribeClick();
// ๐ฏ Custom event type
type GameEvent = CustomEvent<"game", {
action: "start" | "pause" | "end";
score?: number;
level?: number;
}>;
// Extend union type
type ExtendedAppEvent = AppEvent | GameEvent;
// Helper type to extract event payload
type EventPayload<T extends AppEvent, K extends AppEvent["type"]> =
T extends { type: K } ? T["payload"] : never;
// Usage
type ClickPayload = EventPayload<AppEvent, "click">; // { x: number; y: number; target: string; }
๐ Key Takeaways
Youโve mastered TypeScript type aliases! Hereโs what you can now do:
- โ Create type aliases for any type ๐ช
- โ Build union types for flexible values ๐ฏ
- โ Combine types with intersections ๐๏ธ
- โ Use generic aliases for reusable types ๐ก๏ธ
- โ Design better APIs with clear type names! ๐
Remember: Type aliases make your code more readable and maintainable - use them to express your intent clearly! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve become a type alias expert!
Hereโs what to do next:
- ๐ป Complete the event system exercise
- ๐๏ธ Refactor existing code to use type aliases
- ๐ Learn about conditional types
- ๐ Explore template literal types!
Remember: Great types lead to great code. Keep creating those aliases! ๐
Happy coding! ๐๐โจ