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 registers Name in the type space of the current scope, without emitting any JavaScript for it.
  • 2. Substitution on use. Every time Name appears 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 type statement is deleted. There is no Point or Result object 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 type for unions, intersections, tuples, function types, and primitive aliases — interface can’t express these.
  • Use interface for 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 status or kind) 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 plain number is still assignable to UserId. If you need that guarantee, look into branded/nominal typing patterns.
  • Keep widely-shared aliases in a dedicated types.ts file so they’re easy to find and reuse consistently across a project.

Practice Exercises

  • Exercise 1: Write a type alias Temperature that is a union of the literal string types "celsius" and "fahrenheit". Then write a function describeUnit(unit: Temperature): string that 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 a string name and a number score, and log it.
  • Exercise 3: Define a discriminated union Shape made of aliases Circle (kind: "circle", radius: number) and Rectangle (kind: "rectangle", width: number, height: number). Write a function area(shape: Shape): number that 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.
  • type can alias unions, intersections, tuples, and function types — things interface cannot express.
  • interface supports declaration merging (reopening to add members); type does 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.