# Mastering TypeScript: Interfaces, Generics, Unions Explained

When I first started using TypeScript, I honestly thought it was going to be a lot more complicated than JavaScript.

There were types everywhere.

```ts
string
number
boolean
interface
type
union
generic
```

And then I opened a real TypeScript project and saw:

```text
tsconfig.json
```

At that point I was like…

**Okay, what exactly am I supposed to learn first? 😄**

I already knew JavaScript.

So the question that kept coming back was:

**If JavaScript already works, why was TypeScript created?**

That question made much more sense to me than simply starting with TypeScript syntax.

Because TypeScript isn't really asking us to forget JavaScript.

It's trying to make JavaScript applications easier to understand and maintain as they become bigger.

And once I started looking at it that way…

interfaces, unions, intersections, generics, and `tsconfig.json` started feeling less like random TypeScript features.

They started solving actual problems.

* * *

# If JavaScript Works, Why TypeScript?

JavaScript is flexible.

Very flexible.

You can write:

```js
let age = 25;
```

and later:

```js
age = "twenty five";
```

JavaScript doesn't stop you while writing the code.

And that's one of the things people like about JavaScript.

But that flexibility can also become difficult in a large application.

Imagine working on a project with hundreds of files.

You have:

```text
Users
Products
Orders
Payments
Authentication
Dashboard
API
Database
```

Now imagine a function expects:

```js
user.name
user.email
user.age
```

but somewhere else, someone passes an object without `email`.

JavaScript may not complain until that code actually runs.

That's the difference that started making TypeScript useful to me.

* * *

# Runtime Errors vs Compile-Time Errors

![](https://cdn.hashnode.com/uploads/covers/69514a87560e53902880bfd9/fe3f8d3f-5cae-44f7-9d9e-976d7d998047.png align="center")

With plain JavaScript, some problems are discovered when the program runs.

For example:

```js
function greet(user) {
  return user.name.toUpperCase();
}
```

This looks fine.

But what if:

```js
greet({
  age: 25
});
```

Now `user.name` doesn't exist.

The problem appears at runtime.

TypeScript gives us an opportunity to catch many such mistakes earlier.

For example:

```ts
function greet(user: User) {
  return user.name.toUpperCase();
}
```

Now TypeScript knows what kind of value `user` is supposed to be.

That's the part I started appreciating.

Instead of:

```text
Write code
   ↓
Run application
   ↓
Find mistake
```

we can often get:

```text
Write code
   ↓
TypeScript checks it
   ↓
Catch mistake earlier
   ↓
Run application
```

That's a pretty useful developer experience.

* * *

# TypeScript Is Still JavaScript

This was important for me to understand.

TypeScript isn't a completely unrelated language.

It builds on JavaScript.

You can write:

```ts
const name: string = "Sahil";
```

The JavaScript part is still familiar.

The extra part is the type information.

And eventually TypeScript code is converted into JavaScript.

So the browser doesn't need to understand:

```ts
const name: string = "Sahil";
```

directly.

It eventually gets JavaScript.

That means I started thinking of TypeScript as:

**JavaScript with an additional type system and developer tooling.**

That mental model made learning it much easier.

* * *

# Type Annotations

The first TypeScript feature that usually makes sense is type annotations.

For example:

```ts
let username: string = "Sahil";
```

Here:

```text
username
   ↓
string
```

We're telling TypeScript:

**This variable should contain a string.**

Another example:

```ts
let age: number = 25;
```

And:

```ts
let isOnline: boolean = true;
```

Pretty straightforward.

* * *

# Function Parameters

Types become even more useful with functions.

JavaScript:

```js
function add(a, b) {
  return a + b;
}
```

TypeScript:

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

Now TypeScript knows:

```text
a → number
b → number
```

So if I accidentally do:

```ts
add("10", 20);
```

TypeScript can tell me there's a type mismatch.

That's one of the main benefits.

* * *

# Return Types

We can also describe what a function returns.

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

The final:

```ts
: number
```

means the function is expected to return a number.

Sometimes I explicitly write return types.

Sometimes I let TypeScript figure them out.

And that brings us to type inference.

* * *

# Type Inference

TypeScript doesn't always need us to write every type.

For example:

```ts
const name = "Sahil";
```

TypeScript can infer:

```text
name → string
```

And:

```ts
const age = 25;
```

TypeScript understands:

```text
age → number
```

So we don't necessarily need:

```ts
const name: string = "Sahil";
const age: number = 25;
```

everywhere.

This was actually nice to discover.

TypeScript isn't forcing me to write types for absolutely everything.

It can often figure them out.

* * *

# Explicit vs Inferred Types

So I started thinking about it like this:

### Explicit

```ts
const age: number = 25;
```

I'm telling TypeScript the type.

### Inferred

```ts
const age = 25;
```

TypeScript figures it out.

Both are useful.

The important thing is knowing when an explicit type makes the code clearer.

* * *

# Interfaces

Then I got to interfaces.

This is where TypeScript started feeling much more useful for application development.

Imagine we have a user.

In JavaScript:

```js
const user = {
  name: "Sahil",
  age: 25,
  email: "sahil@example.com"
};
```

In TypeScript, we can describe that structure:

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

Now we can use it:

```ts
const user: User = {
  name: "Sahil",
  age: 25,
  email: "sahil@example.com"
};
```

The interface is basically describing the shape of the object.

That's how I started thinking about interfaces:

**An interface describes what an object should look like.**

* * *

# Why Is This Useful?

Imagine a project where many functions work with users.

Instead of repeatedly thinking:

```text
Does User have name?

Does User have email?

Is age a number?

What exactly does this object contain?
```

we can have one clear definition:

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

Now that structure becomes part of the codebase.

And when another developer uses `User`, they have a clear idea of what it represents.

That's where TypeScript starts helping with maintainability.

* * *

# Type Aliases

Then I found another way to describe the same kind of object.

A type alias.

```ts
type User = {
  name: string;
  age: number;
  email: string;
};
```

At first I wondered:

**Why do we have both** `interface` **and** `type`**?**

Because there is a lot of overlap.

Both can describe object shapes.

For example:

```ts
interface Product {
  name: string;
  price: number;
}
```

and:

```ts
type Product = {
  name: string;
  price: number;
};
```

Both can be used to describe a `Product`.

So what's the difference?

* * *

# Interface vs Type Alias

![](https://cdn.hashnode.com/uploads/covers/69514a87560e53902880bfd9/df8807bc-0447-4ba5-9d54-96f4b9031b00.png align="center")

For basic object structures, they can look almost identical.

But they have different capabilities and syntax.

Interfaces are designed around describing object shapes and can be extended or merged in ways that type aliases don't work exactly the same way.

Type aliases are more flexible when you want to describe things beyond plain object shapes.

For example:

```ts
type ID = string | number;
```

That's a type alias describing a union.

You can't express that kind of union simply as an interface.

So the way I started thinking about it was:

```text
interface
→ Great for object contracts

type
→ More flexible for composing different types
```

There isn't always one universally correct answer.

The important thing is consistency within a project and understanding what you're trying to describe.

* * *

# Union Types

![](https://cdn.hashnode.com/uploads/covers/69514a87560e53902880bfd9/77feb317-e63e-4c74-85c8-60c461921370.png align="center")

This was one of the TypeScript features that immediately made sense once I saw a real example.

Suppose an ID can be either a string or a number.

In JavaScript:

```js
let id = 101;
```

Maybe somewhere else:

```js
let id = "user-101";
```

In TypeScript:

```ts
let id: string | number;
```

The `|` means:

**OR**

So:

```text
string | number
```

means:

```text
string
OR
number
```

That's a union type.

* * *

# A Real-World Union

Imagine an API response.

Maybe it can return:

```text
success
```

or:

```text
error
```

We could describe that using different types.

For example:

```ts
type Success = {
  status: "success";
  data: string;
};

type ErrorResponse = {
  status: "error";
  message: string;
};

type Response = Success | ErrorResponse;
```

Now the response can be one of two shapes.

TypeScript can help us handle those possibilities safely.

* * *

# Handling Unions Safely

Suppose:

```ts
function handleResponse(response: Response) {
  if (response.status === "success") {
    console.log(response.data);
  } else {
    console.log(response.message);
  }
}
```

The check:

```ts
response.status === "success"
```

helps TypeScript understand which type we're dealing with.

This is where unions become more than just:

```text
"either this or that"
```

They can help us model real application states.

* * *

# Intersection Types

Then I came across:

```ts
&
```

And I initially thought:

Okay…

another symbol to remember.

But the basic idea is pretty simple.

If union means:

```text
OR
```

intersection means:

```text
AND
```

For example:

```ts
type Person = {
  name: string;
};

type Developer = {
  skills: string[];
};
```

We can combine them:

```ts
type Employee = Person & Developer;
```

Now an `Employee` needs both:

```text
name
+
skills
```

So:

```ts
const employee: Employee = {
  name: "Sahil",
  skills: ["JavaScript", "TypeScript", "React"]
};
```

* * *

# Union vs Intersection

![](https://cdn.hashnode.com/uploads/covers/69514a87560e53902880bfd9/4069aec9-c1fe-4549-8ff5-15ea45c3d8f9.png align="center")

This comparison helped me remember them:

```text
Union

A | B

A OR B
```

Whereas:

```text
Intersection

A & B

A AND B
```

For example:

```ts
type ID = string | number;
```

means:

```text
string OR number
```

But:

```ts
type Employee = Person & Developer;
```

means:

```text
Person AND Developer
```

Once I thought about the symbols that way, I stopped trying to memorize them.

* * *

# Why Do We Need Generics?

Generics were probably the part that looked the most confusing initially.

I saw something like:

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

And my first reaction was:

**What is** `T` **supposed to be?**

Then I realized…

`T` is basically a placeholder for a type.

The function doesn't know the exact type ahead of time.

But it wants to preserve that type.

* * *

# A Simple Generic Function

![](https://cdn.hashnode.com/uploads/covers/69514a87560e53902880bfd9/632d7f4b-72ea-406a-b531-d5092e8b3269.png align="center")

Consider:

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

Now:

```ts
const name = identity("Sahil");
```

TypeScript understands:

```text
T = string
```

So the result is a string.

If we do:

```ts
const age = identity(25);
```

then:

```text
T = number
```

And the result is a number.

So:

```text
string
  ↓
identity<T>
  ↓
string
```

and:

```text
number
  ↓
identity<T>
  ↓
number
```

That's the part that made generics click for me.

* * *

# Why Not Just Use `any`?

You might wonder:

Why not write:

```ts
function identity(value: any) {
  return value;
}
```

The problem is that `any` essentially removes much of the type information.

Generics let us keep the relationship between the input and output.

With:

```ts
function identity<T>(value: T): T
```

TypeScript knows:

**Whatever type comes in, the same type comes out.**

That's much more useful.

* * *

# Generic Constraints

Sometimes we don't want to accept literally anything.

We can put constraints on generics.

For example:

```ts
function getLength<T extends { length: number }>(value: T) {
  return value.length;
}
```

Now `T` must have a `length` property.

So things like strings and arrays can work.

The important idea is:

```text
Generic
+
Constraint
=
Reusable but still type-safe
```

That combination is one of the reasons generics are so useful in larger TypeScript applications.

* * *

# A Practical Generic Example

Imagine a reusable API response.

We might have:

```ts
interface ApiResponse<T> {
  success: boolean;
  data: T;
}
```

Now we can use it with different data.

For users:

```ts
type UserResponse = ApiResponse<User>;
```

For products:

```ts
type ProductResponse = ApiResponse<Product>;
```

The structure stays the same.

Only the data type changes.

That's a great example of why generics exist.

Instead of writing:

```text
UserResponse
ProductResponse
OrderResponse
```

with duplicated structures…

we can create one reusable type.

* * *

# tsconfig.json

Then I opened a TypeScript project and saw:

```text
tsconfig.json
```

Again…

another file I had to understand.

The basic idea is actually straightforward.

`tsconfig.json` tells TypeScript how the project should be compiled and checked.

For example:

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true
  }
}
```

Now TypeScript has project-wide instructions.

* * *

# Why Do We Need tsconfig.json?

![](https://cdn.hashnode.com/uploads/covers/69514a87560e53902880bfd9/bb613b16-1fcb-454f-9f5b-ebc3e659cf4d.png align="center")

Imagine a project with:

```text
src/
 ├── app.ts
 ├── user.ts
 ├── product.ts
 └── server.ts
```

We don't want to manually tell TypeScript every time:

```text
Compile these files.

Use this JavaScript version.

Use these module settings.

Enable these checks.
```

Instead, we put those project settings in:

```text
tsconfig.json
```

Then the compiler knows how the project should be handled.

* * *

# Target

One option you might see is:

```json
{
  "compilerOptions": {
    "target": "ES2022"
  }
}
```

The target controls the JavaScript version TypeScript should produce.

So conceptually:

```text
TypeScript
    ↓
Compiler
    ↓
Target JavaScript version
```

The exact target you choose depends on the environments you're supporting.

* * *

# Module

Another common setting is:

```json
{
  "compilerOptions": {
    "module": "NodeNext"
  }
}
```

This controls how modules are handled in the generated JavaScript and how TypeScript understands the project's module system.

Again…

I don't think you need to memorize every possible value while learning the fundamentals.

It's more important to understand what the setting is responsible for.

* * *

# Strict Mode

Then there is:

```json
{
  "compilerOptions": {
    "strict": true
  }
}
```

This enables a stronger set of type-checking rules.

At first, strict mode can feel annoying.

You write something.

TypeScript complains.

You think:

**Why is TypeScript being so difficult? 😄**

But that's actually part of the value.

Those errors are often pointing out places where the code could be unsafe or unclear.

Over time, I started seeing strict mode less as an obstacle and more as another layer of protection.

* * *

# What Actually Happens To TypeScript?

![](https://cdn.hashnode.com/uploads/covers/69514a87560e53902880bfd9/2c2a8c80-6eba-4698-a2e8-2abf2b6aa229.png align="center")

Now comes the question:

If browsers understand JavaScript…

what happens to my `.ts` files?

A browser can't simply take:

```ts
const age: number = 25;
```

and execute the type annotation as JavaScript.

TypeScript needs to be transformed into JavaScript.

So the simplified process is:

```text
TypeScript
     ↓
Type checking
     ↓
TypeScript compiler
     ↓
JavaScript
     ↓
Browser / Node.js
```

That's the important picture.

* * *

# TypeScript Compilation

Imagine we have:

```text
src/
 └── app.ts
```

The TypeScript compiler processes it.

Eventually we might get:

```text
dist/
 └── app.js
```

So:

```text
app.ts
  ↓
TypeScript Compiler
  ↓
app.js
```

The browser runs the JavaScript output.

This was another important realization for me.

**TypeScript is mainly helping during development and build time.**

The runtime environment still executes JavaScript.

* * *

# A Real Project View

![](https://cdn.hashnode.com/uploads/covers/69514a87560e53902880bfd9/59965b04-d7fe-4c0b-a7e5-f1f9d8998435.png align="center")

Now imagine a slightly bigger project:

```text
my-project/
│
├── src/
│   ├── app.ts
│   ├── user.ts
│   └── product.ts
│
├── dist/
│   ├── app.js
│   ├── user.js
│   └── product.js
│
├── package.json
└── tsconfig.json
```

The general idea is:

```text
src/*.ts
    ↓
TypeScript Compiler
    ↓
dist/*.js
```

The source files are where we write TypeScript.

The output files are JavaScript.

And `tsconfig.json` helps control how that transformation happens.

* * *

# TypeScript Is More Than Just Types

This is probably the biggest thing I took away from learning it.

At first, I thought:

```text
TypeScript
=
JavaScript + types
```

That's technically a useful starting point.

But as I used it more, I started seeing the bigger benefit.

TypeScript gives the developer tools more information.

Your editor can understand:

```text
What properties exist?

What type is this?

What arguments does this function expect?

What does this function return?

What can this variable contain?
```

That can make autocomplete, navigation, refactoring, and error detection much better.

And in a large codebase…

that becomes really useful.

* * *

# One Thing I Started Doing Differently

Before TypeScript, I often thought about objects like:

```text
"Okay, this object probably has these properties."
```

With TypeScript, I started thinking:

```text
"What is the shape of this data?"
```

For example:

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

Now the shape is explicit.

If another part of the application uses `Product`, I don't have to guess what it contains.

That's a small change in thinking.

But it becomes very useful as projects grow.

* * *

# TypeScript Started Feeling Like Documentation

This was something I didn't expect.

Consider:

```ts
function createOrder(
  userId: number,
  productId: number,
  quantity: number
): Order {
  // ...
}
```

Just by reading the function signature, I already know a lot.

I don't necessarily need to open the implementation.

The types tell me:

```text
userId → number
productId → number
quantity → number
returns → Order
```

That's one reason I like TypeScript.

The code can explain itself a little more.

* * *

# What Finally Made TypeScript Click

Initially, I was learning TypeScript feature by feature.

```text
Types
Interfaces
Unions
Intersections
Generics
tsconfig
```

It felt like a list.

Then I started seeing the relationship between them.

Types describe data.

Interfaces describe object structures.

Unions describe multiple possible types.

Intersections combine types.

Generics let us build reusable type-safe code.

And `tsconfig.json` controls how the TypeScript project is checked and compiled.

Suddenly…

they weren't random features anymore.

They were tools for solving different problems.

* * *

# A Simple TypeScript Mental Model

This is the mental model I keep now:

```text
             TypeScript
                  │
       ┌──────────┼──────────┐
       ↓          ↓          ↓
    Type Safety  Structure  Reuse
       │          │          │
       ↓          ↓          ↓
   Annotations Interfaces  Generics
                Types
                  │
          ┌───────┴───────┐
          ↓               ↓
       Unions       Intersections
                  │
                  ↓
              tsconfig
                  │
                  ↓
             Compilation
                  │
                  ↓
              JavaScript
```

I don't need to memorize every feature at once.

I just need to understand what problem each feature solves.

* * *

# Quick Recap

*   TypeScript adds static type checking to JavaScript development.
    
*   It can catch many type-related problems before runtime.
    
*   Type annotations explicitly describe types.
    
*   Type inference lets TypeScript figure out many types automatically.
    
*   Interfaces describe object shapes.
    
*   Type aliases can also describe object shapes and are more flexible for unions and other type compositions.
    
*   Union types use `|` and represent multiple possible types.
    
*   Intersection types use `&` and combine multiple type definitions.
    
*   Generics allow reusable code while preserving type information.
    
*   Generic constraints let us restrict what types can be used.
    
*   `tsconfig.json` contains project-wide TypeScript compiler settings.
    
*   Options such as `target`, `module`, and `strict` influence how TypeScript behaves.
    
*   TypeScript is transformed into JavaScript before it runs in environments that execute JavaScript.
    
*   TypeScript also improves the developer experience through better tooling, autocomplete, and refactoring support.
    

That's the foundation.

* * *

# Conclusion

When I first started learning TypeScript, I thought the goal was simply to add types to JavaScript.

And yes…

that's part of it.

But I think the bigger value is having a better understanding of the data flowing through an application.

Instead of:

```text
"What does this object contain?"
```

I can look at:

```ts
interface User {
  name: string;
  email: string;
}
```

Instead of:

```text
"What types can this function accept?"
```

the function can tell me.

Instead of duplicating similar type structures everywhere, generics can help me build reusable definitions.

And instead of guessing how the project should compile, `tsconfig.json` gives the project a clear configuration.

The biggest thing I learned is that TypeScript isn't something I need to use by memorizing every feature.

It's more useful to think:

**What problem am I trying to prevent or solve?**

Need to describe an object?

Use an interface or type.

Need multiple possible types?

Use a union.

Need to combine structures?

Use an intersection.

Need reusable type-safe logic?

Use generics.

Need project-wide compiler behavior?

That's where `tsconfig.json` comes in.

And eventually all of that TypeScript gets turned into JavaScript that the runtime can actually execute.

So now when I see:

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

function getUser<T>(data: T): T {
  return data;
}
```

I don't see a bunch of complicated TypeScript syntax anymore.

I see a way of making the code more explicit.

And honestly…

that's what made TypeScript start feeling useful to me.

Not because it prevents every bug.

Not because every variable needs a type.

But because as an application grows, **knowing what your data looks like becomes incredibly valuable.**

That's the part I wanted to understand before moving deeper into TypeScript.

If you like this simple learning-style explanation, I write more notes at **devwithsahil.hashnode.dev** and share my progress on LinkedIn 🙂

#typescript #javascript #webdevelopment #frontend #programming
