Engineering Notes
Home/Languages/TypeScript/Type Aliases
Languages · TypeScript

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.

💻 Languages● Beginner⏱ 6 min read🛠️ 2 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • What a Type Alias Is
  • Type Aliases for Object Shapes — Interchangeable with Interfaces
  • type vs interface — 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.

ts
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 type

2Type Aliases for Object Shapes — Interchangeable with Interfaces

ts
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 use

For 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

typeinterface
Object shapes✅✅
Unions (A \| B)✅❌
Tuples✅❌
Primitives/aliases (type X = string)✅❌
Declaration merging❌ (duplicate name = error)✅ (see Interfaces topic)
extends-style combinationvia & intersectionvia 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

ts
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

ts
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

ts
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).

text
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 to
type ID is substituted with its definition wherever referenced, as if you'd written number | string directly
type ID is substituted with its definition wherever referenced, as if you'd written number | string directly

This is also why recursive type aliases need an object (or tuple) boundary

ts
type Bad = Bad; // ❌ error: circular reference, no object boundary to stop substitution
type TreeNode = { children: TreeNode[] }; // ✅ fine — the object literal boundary stops infinite substitution

A 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

  1. Trying to merge two type declarations — always a "duplicate identifier" error; if you need extensibility, that's exactly the case interface exists for.
  2. Assuming type and interface are always interchangeable — they are for plain object shapes only; unions/tuples require type, and consumer-extensible public APIs usually favor interface.
  3. Writing a bare self-referential alias with no structural boundary — type X = X always errors; recursion needs to be guarded inside an object/array/function type.
  4. Forgetting intersections (&) can produce never — combining two object types with the SAME property name but incompatible types collapses that property's type to never, often surprising when it happens accidentally through generic composition.

📋 Quick Reference

text
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

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.