5.2 type: Alias for Any Type Expression
Right, so you’ve met interface. It’s great, we love it. But sometimes you need to create a name for something that isn’t an object shape. Maybe it’s a union type, a function signature, a tuple, or some complex mapped type wizardry. That’s where type aliases come in. Think of them as your personal shorthand for any type expression you can dream up. They’re not a new type themselves—they’re just a new name for an existing type definition. The compiler replaces the alias with its definition during type checking, which is why they’re so powerful and flexible.
The Basic Syntax: It Couldn’t Be Simpler
You use the type keyword, give it a name (in PascalCase by convention, to distinguish it from variable names), an equals sign =, and then the type expression it represents. It’s a direct assignment.
// Aliasing a union type
type UserID = string | number;
// Aliasing a function signature
type StringReverser = (input: string) => string;
// Aliasing a tuple for, say, coordinates
type Coordinate = [number, number];
function moveTo(coord: Coordinate) {
const [x, y] = coord;
// move logic here
}
moveTo([10, 20]); // Works
moveTo([10]); // Error: Source has 1 element(s) but target requires 2.
type vs. interface: The Eternal, Mostly Pointless Debate
You’ll see this debate rage online. Let’s cut through the noise. For describing object types, they are almost identical. You can use either. The key differences are pragmatic:
- Interfaces are extendable; type aliases are not. You can
extendsan interface to create a new one. A type alias is a final, flat definition. You can use intersection types (&) with types to a similar effect, but it’s not the same. - Interfaces are open; type aliases are closed. This is the big one. You can redeclare an interface to add new properties. This is called “declaration merging” and it’s how TypeScript itself adds new properties to built-in types like
Window. A type alias can’t be changed after it’s declared.
// Declaration merging with interface - totally valid
interface Person {
name: string;
}
interface Person {
age: number;
}
const person: Person = { name: "Alice", age: 30 }; // Must have both
// Try that with a type alias - it'll yell at you
type Person = { name: string };
type Person = { age: number }; // Error: Duplicate identifier 'Person'.
So, the best practice? Use interface for object types that represent the public API of your code, especially if you’re writing a library and want to leave the door open for users to extend them. Use type for everything else: unions, tuples, mapped types, and complex compositions. For simple object shapes, just pick one and be consistent.
The Power Move: Generic Type Aliases
This is where type truly shines. You can create parameterized type aliases, just like generic functions or interfaces. This allows you to create powerful, reusable type templates.
// A generic type for an API response wrapper
type APIResponse<T> = {
status: number;
data: T;
error?: string;
};
// Now we can use it for any data type we want
type UserResponse = APIResponse<{ id: number; name: string }>;
type ProductResponse = APIResponse<{ sku: string; price: number }>;
// This function now confidently knows what it's returning
async function fetchUser(id: number): Promise<UserResponse> {
// ... fetch logic
}
The Pitfall: Recursive Type Aliases
Sometimes you need a type that references itself, like for trees or linked lists. Type aliases handle this beautifully, but you must do it correctly. The alias itself cannot appear directly in its own definition. Instead, you must put the recursive reference inside an object or array—a technique called “deferring” the reference.
// Correct: The recursion is inside an object property
type TreeNode<T> = {
value: T;
left?: TreeNode<T>; // This is okay, it's a child property
right?: TreeNode<T>;
};
// ERROR: A type alias cannot directly reference itself
type InfiniteList<T> = T | InfiniteList<T>; // Type alias 'InfiniteList' circularly references itself.
// Correct: Defer the reference by putting it in a tuple (which is an array)
type ValidInfiniteList<T> = T | [ValidInfiniteList<T>];
The rule of thumb is simple: the compiler needs a structural boundary to break the immediate circularity. An object, an array, a function—anything that creates a layer of indirection—will do the trick.
In short, type is your go-to tool for giving a meaningful name to any type structure, simple or complex. It makes your code more readable, more maintainable, and frankly, more fun to write. Don’t just type out string | number | null everywhere. Give it a proper name. Your future self, trying to understand your code at 2 AM, will thank you.