Skip to main content

Command Palette

Search for a command to run...

Mastering TypeScript: Interfaces, Generics, Unions Explained

Updated
7 min readView as Markdown
Mastering TypeScript: Interfaces, Generics, Unions Explained

If JavaScript works, why was TypeScript created?

JavaScript is the undisputed language of the web, but it was originally designed to add simple interactivity to web pages—not to build massive, enterprise-scale applications. Because JavaScript is a dynamically typed language, it is incredibly flexible. However, that flexibility comes at a cost: it won't warn you if you pass a string into a function that expects a number until you actually run the code and it crashes.

TypeScript was built to solve this exact problem. It is not a fundamentally new programming language; rather, it is a superset of JavaScript and a powerful developer tool. It adds static typing to JavaScript, catching errors at compile-time (while you are typing in your editor) rather than at runtime (when your user is interacting with your app).

Let's explore the core features of TypeScript that will dramatically improve your code's safety, maintainability, and your overall developer experience.


1. Why TypeScript Exists

In plain JavaScript, if you misspell an object property or pass the wrong argument type, the browser will likely throw a runtime error (Uncaught TypeError: Cannot read property...). In large applications with dozens of developers, tracking down these bugs is time-consuming and expensive.

The Benefits of Static Typing:

  • Compile-Time Errors: TypeScript acts as a strict proofreader. If you try to call .toUpperCase() on a number, TypeScript will underline it in red in your editor before you even save the file.

  • Developer Productivity: By understanding the exact "shape" of your data, TypeScript supercharges your editor's autocomplete (IntelliSense). You no longer need to constantly check documentation to remember if a user object has an id or a userId.

  • Self-Documenting Code: Types serve as living documentation that never goes out of date.


2. Understanding Type Annotations

Type annotations are how you explicitly tell TypeScript what kind of data a variable or function should hold.

JavaScript vs. TypeScript

Here is a simple function in plain JavaScript:


function calculateTotal(price, tax) {
  return price + tax;
}
// What happens if someone does this?
calculateTotal(100, "20"); // Returns "10020" instead of 120

Here is the same function with TypeScript annotations:


function calculateTotal(price: number, tax: number): number {
  return price + tax;
}

// calculateTotal(100, "20"); //  Error: Argument of type 'string' is not assignable to parameter of type 'number'.

Explicit vs. Inferred Types

You don't have to annotate everything. TypeScript is smart enough to infer types in many cases.

// Explicitly stating the type
let username: string = "Alice"; 

// TypeScript infers this is a string automatically
let userCity = "Seattle"; 

3. Interfaces vs. Type Aliases

When defining the shape of an object, you have two primary tools: Interfaces and Type Aliases.

Interfaces

Interfaces are used specifically to define the shape of an object. Think of them as blueprints.

interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

const customer: User = {
  id: 1,
  name: "John Doe",
  email: "john@example.com",
  isActive: true
};

Type Aliases

Type aliases (type) can also define object shapes, but they are more versatile. They can be used for primitives, unions, and tuples.

type Product = {
  id: string;
  price: number;
};

Which should you use?

Feature interface type
Object Models Yes Yes
Union/Intersection No Yes
Declaration Merging Yes (can be reopened and extended) No (closed once defined)

Rule of thumb: Use interface for defining standard object shapes (like a User or Order), and use type when you need complex combinations (like unions).


4. Union Types

Sometimes, a value can legitimately be more than one type. Union types allow you to combine multiple possible types using the pipe | symbol.

Real-World Use Case

Imagine a product ID that could be a database integer or a UUID string.

type ProductID = number | string;

function fetchProduct(id: ProductID) {
  // We must handle both possibilities safely (Type Narrowing)
  if (typeof id === "string") {
    console.log(`Fetching string ID: ${id.toUpperCase()}`);
  } else {
    console.log(`Fetching number ID: ${id.toFixed(2)}`);
  }
}

Union types are also excellent for defining strict states, such as type Status = "pending" | "approved" | "rejected";.


5. Intersection Types

While unions say "this OR that," intersection types use the & symbol to say "this AND that." They are perfect for combining multiple type definitions into one reusable structure.

type StandardUser = {
  name: string;
  email: string;
};

type AdminPermissions = {
  canDeleteUsers: boolean;
  canManageRoles: boolean;
};

// Creating a new type by merging the two
type SuperAdmin = StandardUser & AdminPermissions;

const admin: SuperAdmin = {
  name: "Sarah",
  email: "sarah@company.com",
  canDeleteUsers: true,
  canManageRoles: true
};

6. Generic Functions

Generics are often the most intimidating part of TypeScript, but they are incredibly powerful.

Imagine a function that takes an item and puts it in a box. In plain JS, the box accepts anything. In strict TS, you might be tempted to use the any type, but any defeats the purpose of TypeScript because you lose autocomplete.

Generics act as a placeholder for a type. They allow you to build a reusable function that locks in the type at the moment you use it.

// The <T> is a placeholder (Type Variable)
function putInBox<T>(item: T) {
  return { content: item };
}

// When we call it, T becomes 'string'
const stringBox = putInBox<string>("Keyboard");
console.log(stringBox.content.toUpperCase()); // Safe! TS knows it's a string.

// Here, T becomes 'number'
const numberBox = putInBox<number>(42);
// console.log(numberBox.content.toUpperCase()); // Error: numbers don't have toUpperCase.

Generic Constraints

You can also limit what a generic can be using the extends keyword. For example, <T extends id: number { }> ensures that whatever type is passed in, it must have an id property.


7. Understanding tsconfig.json

Because TypeScript is a developer tool, it requires configuration. When you initialize a project, you generate a tsconfig.json file. This is the command center for your compiler.

Key configurations include:

  • target: Tells the compiler which version of JavaScript to output (e.g., ES2015, ES6, or ESNext).

  • module: Defines the module system (e.g., CommonJS for Node.js, ESNext for modern frontend).

  • strict: When set to true, this enables a suite of strict type-checking options. (Always leave this on for a safe codebase).

  • outDir: The folder where the compiled JavaScript files will be saved.


8. The TypeScript Compilation Process

Browsers and Node.js cannot run TypeScript directly. They only understand JavaScript.

Therefore, TypeScript must undergo a compilation (or transpilation) process:

  1. Parsing: The TypeScript compiler (tsc) reads your .ts files.

  2. Type Checking: It analyzes the code against your interfaces, unions, and generics to ensure there are no logical or structural errors.

  3. Emission: If everything is correct, it strips away all the type annotations, interfaces, and generics, outputting clean, standard .js files based on your tsconfig.json target.

This build workflow ensures that by the time your code reaches production, it is standard, highly optimized JavaScript, but fortified by the rigorous checks it passed during development.