Type Aliases
A type alias gives a name to any type expression — not just object shapes. This is the key difference from interface, which can only describe object/function/class shapes.
🎯 What you'll learn
- What a Type Alias Is
- Type Aliases for Object Shapes — Interchangeable with Interfaces
typevsinterface— the Real Differences- Combining Type Aliases
- Recursive Type Aliases
1What a Type Alias Is
A type alias gives a name to any type expression — not just object shapes. This is the key difference from interface, which can only describe object/function/class shapes.
type ID = number | string; // a union — interfaces cannot express this directly
type Point = { x: number; y: number }; // an object shape — interfaces CAN express this too
type Coordinates = [number, number]; // a tuple — interfaces cannot express this directly
type Callback = (data: string) => void; // a function type2Type Aliases for Object Shapes — Interchangeable with Interfaces
type User = { id: number; name: string };
interface UserInterface { id: number; name: string; }
const a: User = { id: 1, name: "Ana" };
const b: UserInterface = { id: 1, name: "Ana" };
// both work identically for basic object-shape useFor plain object shapes, type and interface are functionally interchangeable in everyday use — the real differences (below) only matter for specific situations.
3type vs interface — the Real Differences
type | interface | |
|---|---|---|
| Object shapes | ✅ | ✅ |
Unions (A \| B) | ✅ | ❌ |
| Tuples | ✅ | ❌ |
Primitives/aliases (type X = string) | ✅ | ❌ |
| Declaration merging | ❌ (duplicate name = error) | ✅ (see Interfaces topic) |
extends-style combination | via & intersection | via extends keyword |
| Can be implemented by a class | ✅ (if it describes an object shape) | ✅ |
Practical guideline: use interface for object shapes that library consumers might want to extend or augment; use type for unions, tuples, function types, and anything an interface structurally cannot express.
4Combining Type Aliases
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged; // intersection — must satisfy BOTH shapes
const p: Person = { name: "Ana", age: 30 };This is the type-world equivalent of interface extends — combining requirements — but via the & (intersection) operator instead of a keyword (full depth in the Intersection Types topic).
5Recursive Type Aliases
type TreeNode = {
value: number;
children: TreeNode[]; // refers to itself
};
const tree: TreeNode = {
value: 1,
children: [
{ value: 2, children: [] },
{ value: 3, children: [{ value: 4, children: [] }] },
],
};Type aliases can reference themselves (directly or through other aliases), which is essential for describing trees, linked structures, and recursive JSON shapes.
6Generic Type Aliases
type Box<T> = { value: T };
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "hi" };A type alias can take type parameters exactly like a generic function or interface (full depth in the Generics topic).
Under the HoodWhy Aliases Don't Merge, and What "Alias" Really Means▾
A type alias is a substitution, not a new named entity with its own identity
When the checker encounters type ID = number | string, it doesn't create a new distinct type — it records that the name ID should be substituted with the type expression number | string everywhere it's referenced. Conceptually, every use of ID behaves as if you'd written number | string directly at that spot. An interface, by contrast, creates an actual distinct named object type with its own identity in the checker's internal type graph (see the Type System Fundamentals topic) — which is why interfaces can merge (there's a real, ongoing named entity to keep adding members to) while type aliases cannot (there's no entity to add to — a second type ID = ... would just be redefining what the substitution means, which is treated as a conflicting duplicate declaration instead).
type ID = number | string;
// everywhere "ID" appears, the checker effectively substitutes: number | string
interface Window { title: string }
// "Window" is a real, ongoing named symbol other declarations can add members toThis is also why recursive type aliases need an object (or tuple) boundary
type Bad = Bad; // ❌ error: circular reference, no object boundary to stop substitution
type TreeNode = { children: TreeNode[] }; // ✅ fine — the object literal boundary stops infinite substitutionA type alias that would need to substitute itself with no structural boundary in between (like type Bad = Bad) has no way to terminate — the substitution model would expand forever. Wrapping the recursive reference inside an object or array type gives the checker a concrete structural boundary (it only needs to resolve TreeNode lazily, when actually inspecting a children property, not eagerly expand it infinitely up front) — this is the structural reason recursive aliases work when guarded by a shape, but not standing completely alone.
Union types have no natural single "member list" to merge anyway
Even setting aside the substitution-vs-identity distinction, it's worth noting that declaration merging (accumulating more required members onto one shape) is a concept that only makes sense for object-like types in the first place — merging two definitions of type ID = number | string and type ID = boolean has no sensible combined meaning the way two interface member lists concatenating does. The lack of merging isn't an arbitrary restriction — it reflects that most of what type aliases are used for (unions, tuples, primitives) has no coherent "combine these" operation to begin with.
⚠️ Common Pitfalls
- Trying to merge two
typedeclarations — always a "duplicate identifier" error; if you need extensibility, that's exactly the caseinterfaceexists for. - Assuming
typeandinterfaceare always interchangeable — they are for plain object shapes only; unions/tuples requiretype, and consumer-extensible public APIs usually favorinterface. - Writing a bare self-referential alias with no structural boundary —
type X = Xalways errors; recursion needs to be guarded inside an object/array/function type. - Forgetting intersections (
&) can producenever— combining two object types with the SAME property name but incompatible types collapses that property's type tonever, often surprising when it happens accidentally through generic composition.
📋 Quick Reference
type Name = <any type expression> → unions, tuples, primitives, objects, functions
interface Name { ... } → object/function/class shapes only
type merges? → NO (duplicate = error) interface merges? → YES
type combine → A & B (intersection) interface combine → extends🎤 Interview Q&A
Q1What can type aliases express that interfaces can't?core▾
Unions ('a' | 'b'), primitives, tuples, function types, mapped and conditional types, template literal types, and computed combinations.
- Example?
type Result<T> = { ok: true; value: T } | { ok: false; error: string }.
Q2What are template literal types?core▾
Types built from string templates: `type EventName = on${Capitalize<'click' | 'focus'>} gives 'onClick' | 'onFocus'`. Used for typed routes, CSS units, event names.
- Combined with mapped types? Generate getter names like
getName.
Q3How do you model function types?core▾
type Handler = (event: Event) => void, or call signatures in object types, overloads with multiple signatures, and generic functions <T>(x: T) => T.
- void vs undefined return? void means the return value is ignored.
Q4What are recursive type aliases?senior▾
Types referencing themselves, e.g. type Json = string | number | boolean | null | Json[] | { [k: string]: Json }, used for tree structures.
- Limits? Very deep instantiations can hit compiler limits.
Q5What's a branded type?core▾
type UserId = string & { readonly __brand: unique symbol }: structurally different from plain strings, preventing mixing IDs of different entities, created via a validating function.
- Runtime cost? None; brands exist only in types.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.