TypeScript Type Aliases
A type alias lets you give a name to any type — a primitive, an object shape, a union, a tuple, or a function signature. Think of it like a variable, but for types instead of values: once you write type Point = { x: number; y: number };, you can use Point anywhere a type is expected instead of repeating the full shape. Type aliases don’t create new, distinct types — they’re purely a naming convenience that makes code shorter and easier to read. This lesson covers exactly how aliases work under the hood, when to reach for type instead of interface, and the mistakes beginners commonly run into.
Overview: How Type Aliases Work
When you write type Name = SomeType;, the TypeScript compiler stores Name in what’s called the type space — a separate namespace from the normal JavaScript value space where variables, functions, and classes live. Because these two spaces don’t overlap, you can have a variable named user and a type named User in the same file with no conflict. Every time you reference Name in a type position (a variable annotation, a function parameter, a generic argument, and so on), the compiler substitutes in the type it refers to. The alias itself never exists at runtime — it’s a compile-time-only construct.
Because TypeScript uses structural typing (also called “duck typing”), two aliases that describe the same shape are fully interchangeable, even if they have different names. If type A = { id: number } and type B = { id: number }, a value typed as A can be assigned anywhere a B is expected, because the compiler compares the underlying shapes, not the alias names. This is different from languages with nominal typing, where two differently-named types are never interchangeable even if identical in structure. Understanding this is key: a type alias is a label for the compiler’s convenience, not a runtime tag or a firewall between types.
What Can Be Aliased
Unlike interface, which can only describe the shape of an object (or function/constructor signature), type can name any type expression: primitives (type Age = number;), unions (type Status = "idle" | "loading" | "done";), intersections, tuples, function types, mapped types, and conditional types. This flexibility is the main reason type aliases exist alongside interfaces — some constructs, like unions and tuples, simply have no equivalent interface syntax.
Syntax
type Name<TypeParam1, TypeParam2> = TypeDefinition;
| Part | Meaning |
|---|---|
type |
Keyword that starts a type alias declaration. |
Name |
The alias identifier. Convention is PascalCase, e.g. UserId, ApiResponse. |
<TypeParam1, ...> |
Optional generic type parameters, making the alias reusable across many concrete types. |
= |
Separates the alias name from what it refers to. This is required — unlike interface, which has no =. |
TypeDefinition |
Any valid type expression: an object shape, union, intersection, tuple, function type, etc. |
; |
Terminates the statement (optional, but conventional). |
Examples
Example 1: Aliasing an Object Shape
type Point = {
x: number;
y: number;
};
function printPoint(p: Point): void {
console.log(`(${p.x}, ${p.y})`);
}
const origin: Point = { x: 0, y: 0 };
printPoint(origin);
Output:
(0, 0)
Here Point names an object shape with two required number properties. Any object matching that shape — even one not explicitly annotated as Point — can be passed to printPoint, because TypeScript checks structure, not the alias name.
Example 2: Aliasing a Union
type ID = string | number;
function formatId(id: ID): string {
if (typeof id === "number") {
return `#${id.toString().padStart(4, "0")}`;
}
return id.toUpperCase();
}
console.log(formatId(42));
console.log(formatId("abc"));
Output:
#0042
ABC
This is something interface simply cannot do — there’s no way to declare an interface that means “a string or a number.” Inside the function, TypeScript narrows id based on the typeof check, so each branch only allows the operations valid for that specific type (.padStart for numbers-turned-strings, .toUpperCase for strings).
Example 3: Generic Alias with a Discriminated Union
type Success<T> = {
status: "success";
data: T;
};
type Failure = {
status: "error";
message: string;
};
type Result<T> = Success<T> | Failure;
function handleResult<T>(result: Result<T>): void {
if (result.status === "success") {
console.log("Got data:", result.data);
} else {
console.log("Error:", result.message);
}
}
function fetchUserSync(id: number): Result<{ name: string }> {
if (id === 1) {
return { status: "success", data: { name: "Ada" } };
}
return { status: "error", message: "User not found" };
}
handleResult(fetchUserSync(1));
handleResult(fetchUserSync(2));
Output:
Got data: { name: 'Ada' }
Error: User not found
This is the pattern most real-world TypeScript code uses for representing “either this or that” results. Result<T> is a generic alias combining two other aliases with a union. The shared, literal status field is called a discriminant — because both branches include it with different literal values, TypeScript can narrow result to exactly Success<T> or Failure inside each if branch, giving you safe access to data or message without a manual type assertion.
Under the Hood: How the Compiler Handles Aliases
It helps to walk through what actually happens when the compiler sees a type alias:
- 1. Registration. When TypeScript parses
type Name = ..., it registersNamein the type space of the current scope, without emitting any JavaScript for it. - 2. Substitution on use. Every time
Nameappears in a type position afterward, the compiler treats it as if the full type definition were written there directly. - 3. Structural comparison. When checking assignability (can this value go in that slot?), the compiler compares the fully-resolved shapes, ignoring alias names entirely — this is what makes structural typing possible.
- 4. Erasure. During compilation to JavaScript, every
typestatement is deleted. There is noPointorResultobject at runtime — only the plain JavaScript values that satisfied those types while the compiler was checking your code.
Recursive Type Aliases
A type alias can refer to itself, but only through some indirection — inside an object, array, tuple, or function type. A direct, unindirected self-reference is not allowed, because the compiler would have no base case to resolve:
type NestedNumbers = number | NestedNumbers[];
const values: NestedNumbers = [1, [2, 3], [4, [5, 6]]];
console.log(values);
Output:
[ 1, [ 2, 3 ], [ 4, [ 5, 6 ] ] ]
Here NestedNumbers refers to itself, but only as the element type of an array — that indirection through [] is what makes it legal.
Type vs Interface
| Capability | type |
interface |
|---|---|---|
| Object shapes | Yes | Yes |
| Unions / intersections | Yes | No |
| Tuples, function types, primitives | Yes | No (function/constructor signatures only) |
| Declaration merging (reopening later) | No | Yes |
| Extending another type | Via & intersection |
Via extends |
Common Mistakes
Mistake 1: Trying to “Reopen” a Type Alias
Unlike interface, a type alias cannot be declared twice to add more members — TypeScript has no merging behavior for aliases.
type Animal = {
name: string;
};
type Animal = {
age: number;
};
This fails with tsc reporting “Duplicate identifier ‘Animal’.” on both declarations. If you need a type that can be extended or added to later (for example, augmenting a library’s types), use interface instead, since interfaces with the same name in the same scope automatically merge:
interface Animal {
name: string;
}
interface Animal {
age: number;
}
const dog: Animal = { name: "Rex", age: 3 };
console.log(dog);
Output:
{ name: 'Rex', age: 3 }
Mistake 2: Direct Self-Reference Without Indirection
A recursive alias needs to “bottom out” through a container type. Referring to itself with no indirection is rejected:
type Recursive = Recursive;
tsc reports “Type alias ‘Recursive’ circularly references itself.” The fix is to route the self-reference through an array, object, or union with a non-recursive branch, as shown in the NestedNumbers example earlier — the number | branch and the [] wrapper both provide the indirection the compiler needs.
Best Practices
- Use
typefor unions, intersections, tuples, function types, and primitive aliases —interfacecan’t express these. - Use
interfacefor plain object/class shapes that consumers of your code (e.g. a published library) might need to extend or augment via declaration merging. - Name aliases in PascalCase, and prefer descriptive names (
UserResponse) over vague ones (Data). - For “either this or that” results, use a discriminated union with a shared literal field (like
statusorkind) so the compiler can narrow automatically. - Don’t rely on aliasing a primitive (e.g.
type UserId = number;) to get type-safety against mixing up different IDs — because of structural typing, a plainnumberis still assignable toUserId. If you need that guarantee, look into branded/nominal typing patterns. - Keep widely-shared aliases in a dedicated
types.tsfile so they’re easy to find and reuse consistently across a project.
Practice Exercises
- Exercise 1: Write a type alias
Temperaturethat is a union of the literal string types"celsius"and"fahrenheit". Then write a functiondescribeUnit(unit: Temperature): stringthat returns"Metric"for"celsius"and"Imperial"for"fahrenheit". - Exercise 2: Create a generic type alias
Pair<A, B>representing a tuple of two possibly-different types. Use it to declare a variable holding a pair of astringname and anumberscore, and log it. - Exercise 3: Define a discriminated union
Shapemade of aliasesCircle(kind: "circle",radius: number) andRectangle(kind: "rectangle",width: number,height: number). Write a functionarea(shape: Shape): numberthat computes the correct area for each.
Summary
- A type alias gives a name to any type using
type Name = ...;— it lives only in the compiler’s type space and is fully erased at runtime. - Because TypeScript is structurally typed, two aliases with the same shape are interchangeable regardless of name.
typecan alias unions, intersections, tuples, and function types — thingsinterfacecannot express.interfacesupports declaration merging (reopening to add members);typedoes not, and duplicate alias declarations are a compiler error.- Recursive aliases are allowed only through indirection (an array, object, or union branch), never as a direct self-reference.
- Discriminated unions — aliases sharing a literal “tag” field — are the standard pattern for representing safely-narrowable variant data.
