C.W.K.
Stream
Lesson 01 of 06 · published

`type` Aliases: Naming the Shapes

~10 min · types-interfaces, type-alias, naming

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"A type alias is just a name for a shape. The shape is the real thing."

What type actually does

A type declaration introduces a new name for an existing type. type UserId = number creates a name UserId that resolves to number wherever it's used. It is not a new type — UserId and number are interchangeable everywhere. The alias is for the reader, not the compiler.

But aliases are not weak. They get powerful when the right-hand side stops being a primitive. type Status = 'idle' | 'loading' | 'done' names a literal union. type Callback = (msg: string) => void names a function signature. type Tree<T> = { value: T; children: Tree<T>[] } names a recursive shape. None of these have an interface equivalent without compromises.

The three places type is the right tool

1. Unions and intersections. type StringOrNumber = string | number, type Combined = A & B. Interfaces can't express unions at all and only barely express intersections (via extends).

2. Derived types from existing types. type UserKeys = keyof User, type UserEmail = User['email'], type Awaited2 = Awaited<Promise<string>>. These computations need typeinterface can't host them.

3. Function types in isolation. type Handler = (event: Event) => void reads cleaner than the equivalent interface (interface Handler { (event: Event): void }, which uses an obscure call-signature syntax).

Where type and interface overlap

For plain object shapes — what most developers reach for most of the time — type and interface are nearly equivalent:

type UserA = { id: number; name: string };
interface UserB { id: number; name: string }

Both compile to the same checks. Both can be extends-ed (interface uses extends User, type uses intersection & User). Both can be exported. The difference is style, not capability — within this overlap.

The rule of thumb: use type whenever the type isn't a plain object shape. Unions, intersections, function types, derived types, conditional types, mapped types — all of these want type. Plain object shapes are the only overlap region, and there it's a style call.

Recursive types in aliases

One of the most useful things about type is that it can refer to itself. type Tree<T> = { value: T; children: Tree<T>[] } defines a recursive tree shape. The recursive reference is fine because TypeScript resolves it lazily. Interfaces can do this too, but the shape declarations of recursive types often read more cleanly as type.

Pippa's confession

cwkPippa's frontend has a types.ts file where most exports are type rather than interface. The reason is mechanical: a lot of the types are unions (type BrainName = 'claude' | 'codex' | 'gemini' | 'ollama') or derived from API responses (type ChatMessage = Awaited<ReturnType<typeof fetchMessage>>). For pure object shapes the codebase still occasionally uses interface, but type is the default because most of what's expressed isn't pure object shape.

Code

Where `type` is the only choice·typescript
// The places `type` is the only choice.

// Union
type Color = 'red' | 'green' | 'blue';

// Intersection
type WithTimestamp<T> = T & { createdAt: Date };

// Function type
type Comparator<T> = (a: T, b: T) => number;

// Derived from another type
type UserPartial = Partial<User>;
type UserKeys = keyof User;
type UserEmail = User['email'];

// Recursive
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

// Conditional
type IsString<T> = T extends string ? true : false;
type A = IsString<'hi'>;  // true
type B = IsString<42>;    // false
Overlap, then difference·typescript
// The overlap with interface, then the difference.

// Plain object shape — both work, choose by style.
type UserA = {
  id: number;
  name: string;
};

interface UserB {
  id: number;
  name: string;
}

// Both can extend.
type UserAWithEmail = UserA & { email: string };
interface UserBWithEmail extends UserB { email: string }

// Now the difference: only `type` can be a union.
type Status = 'idle' | 'loading' | 'done';   // ✅
// interface Status = 'idle' | 'loading' | 'done';  // ❌ syntax error

// And only `interface` can be declared twice and merged.
interface Box { width: number }
interface Box { height: number }   // adds height — merged into the same Box
// type Box = { width: number };
// type Box = { height: number };   // ❌ Duplicate identifier

External links

Exercise

Build a recursive Comment type with id: number, body: string, and replies: Comment[]. Then write a countAll(c: Comment): number function that walks the tree and counts every comment (the root plus all descendants). Can interface express this? Try writing the same shape as an interface and notice what changes.
Hint
The recursive reference works in both type and interface. The difference shows up if you also wanted to union the comment shape with another kind — e.g., type Node = Comment | Reaction. That union belongs to type.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.