# Mastering TypeScript: Interfaces, Generics, and Unions Explained

Last year I sat down to fill my GATE exam registration form, thinking it would take twenty minutes. It did not.

The very first step was to connect my DigiLocker account. Okay, thik hai, security ke liye hoga, they will just fetch my details from there. But then it did not stop at Aadhaar. It asked me to share every single document they wanted from my DigiLocker, and then a drive folder on top of that. Chalo, fine. Next came real-time face recognition, take my photo from different angles, blink my eyes, now smile. For transparency, okay, I get it. Then it asked me to upload my photo and my signature in one exact format, at an exact pixel ratio. Reducing the size, one second ka kaam. But the exact ratio? I had to open Photoshop for that. Then email verification. And through every field of that form, one line kept flashing in my head: one wrong detail and your application gets rejected. So I checked every single thing twice before moving to the next step.

Annoying as it was, here is what that form was actually doing. It refused to let me move forward with a wrong value. Wrong format, wrong ratio, wrong field, it stopped me right there and made me fix it, before submitting, not weeks later when the application silently bounces. That strict "check everything upfront, reject mistakes immediately" behaviour is exactly what TypeScript brings to JavaScript. Chalo, let me show you the whole thing.

## Why TypeScript even exists

To feel why TypeScript exists, you have to first feel the problem with plain JavaScript. JavaScript is the opposite of that GATE form. It lets you write almost anything, accepts it happily, and only screams later, at runtime, when a real user is already on your site.

Look at this.

```javascript
function getTotal(price, quantity) {
  return price * quantity;
}

getTotal(100, 2);      // 200, correct
getTotal(100, "abc");  // NaN, silently wrong
getTotal(100);         // NaN, quantity is undefined
```

JavaScript did not stop you on any of these. It ran all three. The second and third are bugs, they quietly hand back `NaN`, but you only find out when the numbers come out wrong in production, matlab bahut der ho chuki hoti hai. This is the JavaScript form, submit anything, discover the mistake way too late.

TypeScript flips this. It is JavaScript plus a strict checker sitting on top, checking the types of your values before your code ever runs. You tell it "this must be a number," and the moment you try to hand it a string, it stops you right there in your editor, with a red underline, exactly like that form refusing my wrong pixel ratio.

```typescript
function getTotal(price: number, quantity: number) {
  return price * quantity;
}

getTotal(100, 2);      // fine
getTotal("100", "2");  // Error: Argument of type 'string' is not assignable to 'number'
getTotal(100);         // Error: Expected 2 arguments, but got 1
```

Same logic, but now the two buggy calls are caught before you run anything. That is the entire pitch. TypeScript is not a different language you have to relearn, it is JavaScript with a guideline checker attached, catching your mistakes upfront instead of at runtime.

![](https://cdn.hashnode.com/uploads/covers/695029cf9f07c6947bcd9073/655c6848-2252-40f3-bba5-bf855710b1a7.png align="center")

## Type annotations: telling TypeScript the exact format

That GATE form had an exact format for every field. Photo in this ratio, signature in that one, date like this, roll number like that. In TypeScript, the way you set those exact rules is type annotations.

A type annotation is just a colon and a type after your variable, telling TypeScript what kind of value is allowed to live there.

```typescript
let username: string = "Ayush";
let age: number = 21;
let isEnrolled: boolean = true;
```

Now these variables are locked to their type. Try to put the wrong kind of value in, and TypeScript stops you, just like the form rejecting a wrong entry.

```ts
username = "Krishna"; // fine, still a string
username = 42;        // Error: Type 'number' is not assignable to type 'string'
```

The same idea works on function parameters and return values, which is where it really earns its keep.

```typescript
function greet(name: string): string {
  return `Hi ${name}`;
}

greet("Ayush"); // "Hi Ayush"
greet(100);     // Error: name must be a string
```

That `: string` after the brackets is the return type, you are promising this function hands back a string, and TypeScript will check that you actually do.

One lovely thing, you do not always have to write the annotation yourself. TypeScript is smart enough to guess the type from the value, called type inference.

```typescript
let city = "Jodhpur"; // TypeScript already knows this is a string
city = 5;             // Error, even though you never wrote : string
```

So you annotate where it matters, function inputs and outputs mostly, and let inference handle the obvious stuff. Bas, itna hi.

For arrays and objects it is the same feeling, you just describe the shape.

```typescript
let marks: number[] = [88, 92, 76];       // an array of numbers only
let names: string[] = ["Parag", "Chirag"]; // an array of strings only

let user: { name: string; age: number } = {
  name: "Ayush",
  age: 21,
};
```

That last one, an object with an exact required shape, is so important that TypeScript gives us a cleaner way to define it. And that is where interfaces come in.

## Interfaces and type aliases: defining the shape

Writing `{ name: string; age: number }` inline every time is painful, and real apps have the same object shape everywhere. So we give the shape a name once and reuse it. There are two ways to do this, interfaces and type aliases.

Think of it like the document checklist that GATE form demanded. It did not just vaguely want "some documents," it wanted an exact list, each of an exact kind. An interface is that checklist for an object.

```typescript
interface User {
  name: string;
  age: number;
  email: string;
}
```

Now `User` is a reusable shape. Any object claiming to be a `User` must match this checklist exactly, or TypeScript rejects it.

```typescript
const student: User = {
  name: "Ayush",
  age: 21,
  email: "ayush@example.com",
}; // all fields present and correct, accepted

const broken: User = {
  name: "Ayush",
  age: 21,
}; // Error: Property 'email' is missing
```

See that? Miss one required field and it stops you, exactly like the form refusing to move on until every mandatory box is filled.

A **type alias** does a very similar job, it also names a shape, just with slightly different syntax.

```typescript
type Product = {
  title: string;
  price: number;
  inStock: boolean;
};

const item: Product = {
  title: "Notebook",
  price: 60,
  inStock: true,
};
```

So which one do you use? Here is the honest, practical answer, because people overthink this endlessly.

For describing the shape of an object, especially models like `User`, `Product`, `Order`, both work, and interfaces are the common convention. The real difference shows up at the edges. An interface can be extended and even reopened to add fields later, which suits objects and models nicely. A type alias is more flexible for things that are not plain objects, like union types, which we are about to meet, where an interface simply cannot be used.

My simple rule, use `interface` for object and model shapes, use `type` when you need unions or something that is not a straightforward object. You will not go wrong with that.

One more genuinely useful thing, interfaces can build on top of each other with `extends`, so you do not repeat fields.

```typescript
interface Person {
  name: string;
  age: number;
}

interface Employee extends Person {
  employeeId: number;
  salary: number;
}
```

An `Employee` now needs all four fields, the two it inherited from `Person` plus its own two. No copy-paste. This is the same "small pieces building bigger ones" idea from modules, just applied to shapes.

![](https://cdn.hashnode.com/uploads/covers/695029cf9f07c6947bcd9073/6dbf3421-6eba-4c41-bc88-2d6032a6241e.png align="center")

## Union types: allowing a few exact options

Some fields on that form were not free text, they were a fixed set of choices. Category could be General, OBC, SC, ST, nothing else. Gender was a fixed list. You could not type whatever you wanted, only one of the allowed values.

Union types are exactly that. A union says "this value must be one of these specific options." You write it with the pipe `|` symbol, read it as "or."

```typescript
type Status = "pending" | "approved" | "rejected";

let applicationStatus: Status = "pending"; // fine
applicationStatus = "approved";            // fine
applicationStatus = "submitted";           // Error: not one of the allowed options
```

Beautiful, right? `"submitted"` looks reasonable, but it is not on the list, so TypeScript rejects it instantly, just like the form refusing a category that is not in its dropdown. This stops a whole class of silly typo bugs cold.

Unions are not only for fixed strings, they can also allow a value to be one of a few types.

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

printId(101);      // fine
printId("A101");   // fine
printId(true);     // Error: boolean is not allowed
```

Here `id` can be a number or a string, because some systems use numeric IDs and some use codes like "A101," but it can never be a boolean.

Now, one important habit that comes with unions, narrowing. When a value could be more than one type, TypeScript makes you check which one it actually is before you do type-specific things to it.

```typescript
function formatId(id: number | string) {
  if (typeof id === "string") {
    return id.toUpperCase(); // safe, TypeScript knows it is a string here
  }
  return id.toFixed(0);      // safe, here it must be a number
}
```

Inside the `if`, TypeScript is smart enough to know `id` is a string, so string methods are allowed. In the other branch, it knows it must be a number. This "check first, then use" is TypeScript quietly protecting you from calling a string method on a number. Yaad rakhna, unions and narrowing almost always travel together.

## Intersection types: combining shapes together

If a union is "this OR that," an intersection is "this AND that." You combine multiple shapes into one that must satisfy all of them. You write it with the ampersand `&`.

Imagine your app has some basic info and some contact info, defined separately.

```typescript
interface BasicInfo {
  name: string;
  age: number;
}

interface ContactInfo {
  email: string;
  phone: string;
}

type FullProfile = BasicInfo & ContactInfo;
```

Now a `FullProfile` must have every field from both, all four of them.

```typescript
const profile: FullProfile = {
  name: "Ayush",
  age: 21,
  email: "ayush@example.com",
  phone: "9999999999",
}; // needs all four, or TypeScript complains
```

This is genuinely useful in real apps. You keep small, focused shapes, and combine them when you need the full thing, instead of writing one giant repetitive interface everywhere. Chhote tukde, jod ke bada bana lo.

A quick way to hold the two in your head. Union with `|` means "one of these." Intersection with `&` means "all of these at once." That single distinction clears up most of the confusion people have here.

![](https://cdn.hashnode.com/uploads/covers/695029cf9f07c6947bcd9073/baa08441-e1dc-44c5-926f-453d2432e617.png align="center")

## Generic functions: reusable with any type

Now the topic everyone fears, generics. I promise it is simpler than it looks if we build it up slowly.

Start with a real problem. Say you want a function that just returns whatever you give it, the first item of an array, say. You could write it for numbers.

```typescript
function firstNumber(arr: number[]): number {
  return arr[0];
}
```

But now you also need it for strings. And for users. Are you going to write `firstString`, `firstUser`, `firstProduct`, copying the same logic every time? That is exactly the copy-paste mess modules were meant to kill.

The lazy fix is to use `any`, which turns type checking off for that value. But then you throw away all the safety TypeScript gave you, and you are basically back to plain JavaScript. Bekar.

Generics are the real fix. A generic lets you write the logic once and keep it type-safe for any type the caller uses. You introduce a type placeholder, written in angle brackets, usually called `T`, and it stands in for "whatever type gets passed in."

```typescript
function first<T>(arr: T[]): T {
  return arr[0];
}
```

Read `<T>` as "this function works with some type T, I will find out which one when it is called." The input is an array of `T`, and it returns a single `T`. Now watch it flex.

```typescript
first<number>([1, 2, 3]);         // returns a number, 1
first<string>(["a", "b", "c"]);   // returns a string, "a"
first([true, false]);             // T inferred as boolean, no need to write it
```

One function, fully type-safe for every type, no copy-paste, and no throwing away safety with `any`. That last line shows TypeScript inferring `T` on its own, just like it inferred variable types earlier, so you often do not even write the `<...>`.

Here is why this matters so much for real backend work. Think of a function that fetches records from a database. Sometimes it returns users, sometimes products, sometimes orders. A generic lets you type that perfectly.

```typescript
function getRecords<T>(table: string): T[] {
  // pretend this fetches rows and returns them
  return [] as T[];
}

const users = getRecords<User>("users");       // typed as User[]
const products = getRecords<Product>("products"); // typed as Product[]
```

Same function, but `users` is fully typed as `User[]` and `products` as `Product[]`. This exact pattern is everywhere in real Node backends, which is why generics are worth the small mental effort now. Ek baar samajh gaye, to zindagi aasan.

## tsconfig.json: the rulebook

Every strict form runs on a set of guidelines, how strict to be, what is mandatory, what is allowed. For a TypeScript project, that rulebook is a file called `tsconfig.json`. It sits at the root of your project and tells the TypeScript compiler how to behave.

You do not need to memorise it, and I am keeping this high-level on purpose. Here is a small, typical one.

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

Just the ones worth knowing right now. `target` is which version of JavaScript your TypeScript gets turned into. `module` is which module system to use, remember import and export from the last post, this is where that gets configured. `rootDir` is where your TypeScript source lives, and `outDir` is where the compiled JavaScript gets written. And `strict: true` is the big one, it turns on all the strict checks, making TypeScript as careful as that GATE form. Keep it on. Turning it off to silence errors is like bribing the clerk to skip your document check, it just means the bug reaches production instead.

That is really all you need from tsconfig for now. It is the guidelines file, you will tweak it occasionally, not daily.

## The compilation process: how TypeScript actually runs

Here is a fact that surprises a lot of beginners. At their core, browsers and Node run JavaScript, not TypeScript. Some newer setups can take a `.ts` file and run it by quietly stripping the type annotations out first, but stripping is not checking, it just throws the types away so the file can run, it never actually verifies them.

So where does the real checking happen? In a step called compilation. The TypeScript compiler, `tsc`, takes your `.ts` files, checks all the types (this is the form-checking part), and then strips the types away to produce plain `.js` files that Node or the browser can actually run.

```plaintext
your code (.ts)  ->  tsc checks types + compiles  ->  plain JavaScript (.js)  ->  Node/browser runs it
```

This is the key insight about where safety comes from. All that type checking happens at compile time, before your code ever runs. The types are a check, not something that exists when the program is live. Once compiled, the output is ordinary JavaScript with the annotations removed. It is exactly the GATE form checking every field at the counter, before your application is accepted, and the accepted application that moves forward is just clean data.

So if I write this TypeScript,

```typescript
function greet(name: string): string {
  return `Hi ${name}`;
}
```

the compiled JavaScript that actually runs is simply this, types gone,

```typescript
function greet(name) {
  return `Hi ${name}`;
}
```

Same code, minus the guardrails, because the guardrails already did their job during compilation. This is why TypeScript adds zero speed cost at runtime, all the work happened earlier, upfront, before anything ran. Catch it at the counter, not in production.

![]( align="center")

## Try it yourself

Do not just read it, khud chala ke dekho. This is the fastest way to feel TypeScript catching you.

First, one-time setup. In an empty folder, run this.

```plaintext
npm install -g typescript
```

Now make a file called `test.ts` and paste this in.

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

function printUser(user: User): string {
  return `${user.name} is ${user.age}`;
}

const me: User = { name: "Ayush", age: 21 };
console.log(printUser(me));

// now break it on purpose and watch TypeScript complain:
// const broken: User = { name: "Ayush" };   // missing age
// printUser("not a user");                   // wrong type
```

Compile it by running `tsc test.ts`. That produces a `test.js` file. Run it with `node test.js`. Now uncomment those two broken lines and run `tsc test.ts` again, and watch it refuse to compile cleanly, pointing at exactly what is wrong. Play with it, add a union type, add a generic `first` function, break things on purpose. That red underline catching your mistake before it runs is the whole feeling of TypeScript in one moment.

## Wrapping up

So that is TypeScript. It is not a new language to fear, it is JavaScript with a strict checker sitting on top, exactly like that GATE form that refused to let me move ahead with a wrong pixel ratio or a missing document. Type annotations set the exact format for each value. Interfaces and type aliases define the shape of your objects, your `User`, your `Product`, your `Order`. Unions let a value be one of a few exact options, intersections combine shapes into one. Generics let you write reusable, type-safe logic once for any type. tsconfig is the rulebook, and compilation is the counter where every field gets checked before your code is ever accepted to run.

The form was annoying in the moment, sure. But it never let a mistake through to bite me later, and that is precisely the trade TypeScript offers, a little strictness now for a lot fewer 2 AM bugs later.

And here is why I am ending my JavaScript run on this one, right before we go backend. Every real backend I am about to build in the next series is written in TypeScript. Your database models will be interfaces, your API responses will be typed, your functions that fetch records will be generics, your request statuses will be unions. Everything from here on sits on top of exactly what you just learned. So get comfortable with these ideas now, because from the very first backend post, we will be typing everything.

I hope you enjoyed reading this.
