Skip to main content

Command Palette

Search for a command to run...

Mastering TypeScript: Interfaces, Generics, Unions Explained

Updated
•9 min read•View as Markdown
Mastering TypeScript: Interfaces, Generics, Unions Explained

If JavaScript works, why was TypeScript created? This article answers that, then builds up from basic typing to interfaces, generics, and project configuration.

1. Why TypeScript Exists

Problems with Plain JavaScript in Large Applications

JavaScript doesn't check types. A function expecting a number happily accepts a string, an object with a missing property, or undefined — and the mistake only surfaces when that broken code actually runs, sometimes in production, sometimes deep in a call stack far from where the bug originated.

function calculateTotal(price, quantity) {
  return price * quantity;
}

calculateTotal("100", 5); // "100100100100100" — no error, just wrong

In a small script, this is survivable. In an application with hundreds of files and multiple developers, silent type mismatches like this compound into real bugs.

Runtime Errors vs Compile-Time Errors

  • Runtime error: caught only when the broken line executes — potentially after deployment.

  • Compile-time error: caught by the tool before the code ever runs.

function calculateTotal(price: number, quantity: number) {
  return price * quantity;
}

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

TypeScript catches this the moment it's written, not after it ships.

Benefits of Static Typing

  • Errors surface while coding, not in production

  • Autocomplete and inline documentation improve, since the editor knows exact shapes of data

  • Refactoring is safer — renaming a property flags every place it breaks

  • Code becomes self-documenting: function signatures describe what they expect

How TypeScript Improves Developer Productivity

Less time debugging "why is this undefined," more time writing features. The editor catches typos in property names and wrong argument types before you even run the code.

TypeScript as a Superset of JavaScript

Every valid JavaScript file is already valid TypeScript. TypeScript adds a type layer on top — it doesn't replace or reinvent the language.

// Valid JS, also valid TS
let username = "DT89";

2. Understanding Type Annotations

Adding Types to Variables

let age: number = 25;
let name: string = "Tamal";
let isActive: boolean = true;

Function Parameter Types

function greet(name: string) {
  console.log(`Hello, ${name}`);
}

Function Return Types

function add(a: number, b: number): number {
  return a + b;
}

Type Inference

TypeScript often figures out the type without an annotation.

let city = "Kolkata"; // inferred as string, no annotation needed

Explicit vs Inferred Types

Example When to use
Inferred let x = 10; Simple assignments where the value makes the type obvious
Explicit let x: number; Function parameters, return types, and empty declarations TypeScript can't guess

3. Interfaces vs Type Aliases

What Interfaces Are

An interface describes the shape of an object.

interface User {
  name: string;
  age: number;
}

const user1: User = { name: "Aditi", age: 22 };

What Type Aliases Are

A type alias gives a name to any type — object shapes, unions, primitives.

type User = {
  name: string;
  age: number;
};

Similarities

Both describe object shapes and can be used interchangeably for basic cases like the User example above.

Differences

interface type
Extending extends keyword & (intersection)
Merging Can be re-opened and merged Cannot be redeclared
Unions Cannot represent unions Can represent unions directly
Best for Object/class shapes Unions, primitives, complex compositions

When to Use Interfaces

Modeling objects, especially ones that classes will implement:

interface Product {
  id: number;
  name: string;
  price: number;
}

When to Use Type Aliases

Anything involving unions or combining multiple types:

type Status = "pending" | "shipped" | "delivered";

4. Union Types

What Union Types Are

A union type allows a value to be one of several specified types.

let orderStatus: "pending" | "shipped" | "delivered";
orderStatus = "shipped"; // valid

Combining Multiple Possible Types

function printId(id: number | string) {
  console.log(`ID: ${id}`);
}

printId(101);
printId("A101");

Real-World Use Cases

An Order might have a status that's only ever one of a fixed set of strings, or an API response that returns either a User or an Error object.

interface Order {
  id: number;
  status: "pending" | "shipped" | "delivered";
}

Handling Unions Safely

Narrow the type before using it, so TypeScript knows exactly which branch applies.

function handleId(id: number | string) {
  if (typeof id === "number") {
    console.log(id.toFixed(2));
  } else {
    console.log(id.toUpperCase());
  }
}

5. Intersection Types

What Intersection Types Are

An intersection type combines multiple types into one — the result must satisfy all of them.

type Name = { name: string };
type Age = { age: number };

type Person = Name & Age;

const p: Person = { name: "Rahul", age: 21 };

Combining Multiple Type Definitions

type Timestamped = { createdAt: Date };
type Product = { id: number; name: string };

type ProductWithTimestamp = Product & Timestamped;

Creating Reusable Type Structures

Break shared fields into small types, then combine them wherever needed instead of repeating fields across every interface.

Practical Example

type BaseUser = { id: number; name: string };
type AdminPermissions = { canDeleteUsers: boolean; canBanUsers: boolean };

type Admin = BaseUser & AdminPermissions;

const admin: Admin = {
  id: 1,
  name: "Tamal",
  canDeleteUsers: true,
  canBanUsers: true,
};

6. Generic Functions

Why Generics Are Needed

Without generics, a reusable function either loses type safety (using any) or has to be duplicated per type.

function identity(value: any): any {
  return value;
}
// Type safety is gone — value could be anything, output could be anything

Reusable Type-Safe Functions

Generics let a function stay flexible while keeping the input and output types linked.

function identity<T>(value: T): T {
  return value;
}

identity<number>(5); // T = number
identity<string>("hello"); // T = string

T is a placeholder type, filled in based on what's actually passed.

Generic Parameters

function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}

getFirstElement<number>([1, 2, 3]); // 1
getFirstElement<string>(["a", "b"]); // "a"

Generic Constraints

Restrict T to types that have certain properties, using extends.

interface HasId {
  id: number;
}

function printId<T extends HasId>(item: T) {
  console.log(item.id);
}

printId({ id: 101, name: "Order A" }); // valid
printId({ name: "No ID here" }); // Error: missing 'id'

Real-World Example

function wrapInArray<T>(item: T): T[] {
  return [item];
}

wrapInArray<string>("Kolkata"); // ["Kolkata"]
wrapInArray<number>(42); // [42]

7. Understanding tsconfig.json

What tsconfig.json Is

A configuration file at the root of a TypeScript project that tells the compiler how to treat the code — which files to include, which JavaScript version to target, and how strict to be.

Why TypeScript Projects Need It

Without it, every file would need compiler flags passed manually on every run. tsconfig.json centralizes those settings for the whole project.

Common Compiler Options

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

Strict Mode

"strict": true enables a group of stricter checks — no implicit any, stricter null checks, and more — catching more mistakes at the cost of requiring more explicit types.

Target Configuration

"target" decides which JavaScript version the TypeScript compiles down to (e.g., ES5, ES2020), based on which environments the code needs to run in.

Module Configuration

"module" decides the module system used in the compiled output — commonjs for Node.js, esnext for modern bundlers.

Project-Wide Settings

include and exclude control which files/folders the compiler processes:

{
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

8. TypeScript Compilation Process

How TypeScript Becomes JavaScript

TypeScript code is passed through the TypeScript compiler (tsc), which strips out the types and produces plain JavaScript.

input.ts  --(tsc)-->  output.js

What Happens During Compilation

  1. TypeScript checks all type annotations against how values are actually used

  2. If there are type errors, tsc reports them

  3. If everything checks out, tsc strips the types and emits plain JavaScript matching the configured target

Why Browsers Cannot Run TypeScript Directly

Browsers only understand JavaScript. TypeScript's type system exists purely as a development-time tool — it has no meaning at runtime, so it must be compiled away before the code can execute anywhere.

Build Workflow Overview

Write .ts files
      |
      v
Run tsc (or a bundler with a TS plugin)
      |
      v
Type-checking happens
      |
      v
.ts -> .js output (types stripped)
      |
      v
Browser / Node.js runs the plain JS

Summary

Concept Core Idea
Type annotations Declare expected types explicitly
Interfaces Describe object shapes, support merging
Type aliases Name any type, including unions
Union types Value can be one of several types
Intersection types Combine multiple types into one
Generics Reusable functions that stay type-safe
tsconfig.json Central compiler configuration
Compilation .ts → type-checked → .js → runs in browser/Node

Progression: basic typing → interfaces/type aliases → unions/intersections → generics → project-wide configuration. Each layer builds directly on the one before it.

More from this blog