08-typescriptTermsLevel_05Type Aliases (type)

Type Aliases (type)

Level 5 — Union & Intersection Types A way to give a custom name to any valid TypeScript type. It is the primary alternative to interface.


1. Prerequisites


2. Term Category

TypeScript Core Syntax (Custom Type Naming & Computation): Type aliases (type Name = ...) assign names to custom primitives, unions, tuples, intersections, or object shape types.


3. Explanation

Environment Context

  • Compile-Time

(1) Design Motivation — "Why did we design this?"

You are typing an ID parameter as string | number. You have to write string | number in 50 different function signatures. You cannot use an interface to fix this, because interface can only represent Object shapes. It cannot represent primitives or unions. Type Aliases solve this. You use the type keyword to assign a custom name to literally any type configuration.

(2) The Syntax

It looks exactly like declaring a variable with const, but you use type.

// Naming a Union
type ID = string | number;

function printId(id: ID) { ... }

// Naming an Object (Just like an interface)
type User = {
  name: string;
  age: number;
};

(3) Interface vs Type

This is the most common debate in TypeScript. Which one should you use for objects?

  • interface: Can ONLY define objects. Supports extends. Supports Declaration Merging (you can declare it twice to add properties). Often preferred by the TS compiler for error message readability.
  • type: Can define objects, primitives, unions, and tuples. Supports Intersections (&). Does NOT support Declaration Merging (declaring it twice throws an error).

4. Common Mistakes & Pitfalls

Mistake 1: Relying too heavily on type for public library APIs

The mistake: A developer writes a public NPM package and exports all their configuration objects as type Config = { ... }.

Why it's wrong: If a user downloads your package and realizes your Config object is missing a specific property they need for a niche use case, they cannot inject it. If you had exported interface Config, the user could use Declaration Merging to patch your library locally. Golden Rule: For application code, type vs interface is mostly personal preference. For public library APIs, always use interface for object shapes to allow extensibility by the users.


Mistake 2: Attempting Declaration Merging with type Aliases

The mistake: Declaring type User = { name: string }; twice in the same module scope.

Why it's wrong: Type aliases cannot be merged; duplicate type declarations trigger Duplicate identifier compile errors. Use interface if merging is required.

Incorrect:

type Point = { x: number };
// type Point = { y: number }; // ❌ Duplicate identifier 'Point'

Fix:

interface Point { x: number; }
interface Point { y: number; } // Merges successfully

Mistake 3: Creating Direct Non-Optional Recursive Type Aliases without Array/Promise Wrappers

The mistake: Writing type Tree = { parent: Tree }; without optional or container wrapping.

Why it's wrong: Direct self-referencing non-optional types create infinite type resolution loops during compilation.

Incorrect:

// type Node = { child: Node }; // ❌ Type alias 'Node' circularly references itself

Fix:

type Node = { children: Node[] }; // Safe container recursive type

5. Practice Exercises

Exercise 1: Defining Complex Union and Intersection Type Aliases

Scenario: Create type aliases for user IDs (type UserId = string | number) and user records.

Requirements:

  1. Declare primitive union type alias UserId.
  2. Declare object shape type alias User.
Answer

Implementation

type UserId = string | number;

type UserRole = "admin" | "editor" | "viewer";

type User = {
  id: UserId;
  username: string;
  role: UserRole;
};

const user: User = { id: 101, username: "dev_alex", role: "admin" };

Technical Explanation

  1. Type aliases (type Name = ...) assign reusable names to any valid TypeScript type expression.
  2. Capable of representing primitive unions (string | number), literal unions ("admin" | "editor"), and object shapes.
  3. Promotes readable, self-documenting code.

Exercise 2: Defining Generic Type Aliases

Scenario: Create a generic API response wrapper type alias ApiResponse<T>.

Requirements:

  1. Define type ApiResponse<T> = { data: T; status: number; message: string }.
Answer

Implementation

type ApiResponse<T> = {
  data: T;
  status: number;
  message: string;
};

type UserData = { id: number; name: string };

const response: ApiResponse<UserData> = {
  data: { id: 1, name: "Alice" },
  status: 200,
  message: "Success"
};

Technical Explanation

  1. Type aliases accept generic type parameters (<T>) to create reusable parametric type templates.
  2. ApiResponse<UserData> substitutes T with UserData during type resolution.
  3. Essential pattern for wrapping asynchronous API responses.

Exercise 3: Recursive Type Aliases for JSON Structures

Scenario: Define a recursive JSONValue type alias representing arbitrary valid JSON data.

Requirements:

  1. Define recursive union JSONValue.
Answer

Implementation

type JSONPrimitive = string | number | boolean | null;
type JSONObject = { [key: string]: JSONValue };
type JSONArray = JSONValue[];

type JSONValue = JSONPrimitive | JSONObject | JSONArray;

const data: JSONValue = {
  title: "Settings",
  tags: ["json", "typescript"],
  nested: { count: 42 }
};

Technical Explanation

  1. Type aliases can reference themselves recursively inside object shapes or array definitions.
  2. Perfect for modeling recursive data structures like JSON trees or AST nodes.
  3. Advanced type modeling capability.


7. Key Takeaways

  • Type Aliases (type Name = ...) allow you to assign a custom name to any TypeScript type.
  • Unlike interface, Type Aliases can name Primitives, Unions, Tuples, and Functions.
  • Type Aliases do not support Declaration Merging (you cannot declare the same type twice to merge properties).
  • Use interface by default for Object shapes (especially in public libraries), and use type when dealing with Unions, Intersections, or Primitives.
Built with LogoFlowershow