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
TypeScript checks all type annotations against how values are actually used
If there are type errors,
tscreports themIf everything checks out,
tscstrips the types and emits plain JavaScript matching the configuredtarget
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.




