DEV Community

Shivam Yadav
Shivam Yadav

Posted on

Mastering TypeScript: Interfaces, Generics, Unions Explained

If JavaScript works, why was TypeScript created?

Imagine you're building a small calculator app using JavaScript. Everything works perfectly. You write a few functions, display some buttons, and you're done in a couple of hours.

Now imagine you're building something much larger—a banking application, an e-commerce platform like Amazon, or a social media platform like Instagram. Thousands of files. Hundreds of developers. Millions of users.

Suddenly, JavaScript starts showing its limitations.

A small mistake in one file might not be discovered until someone actually uses that feature. A function expecting a number might accidentally receive a string. A property name could be misspelled. An API might return unexpected data.

The browser won't complain until the code runs.

That's where TypeScript changes everything.

TypeScript doesn't replace JavaScript—it improves it. Think of it as a smart assistant that checks your code before it ever reaches the browser. It catches mistakes early, provides better tooling, and makes large projects much easier to maintain.

Today, TypeScript powers projects at companies like Microsoft, Google, Airbnb, Shopify, Discord, and countless startups. If you're learning modern web development with React, Next.js, Angular, or Node.js, TypeScript has become an essential skill.

In this article, we'll build a strong foundation by understanding:

Why TypeScript was created
How static typing works
Type annotations and type inference
Interfaces and type aliases
Union and intersection types
Generic functions
The purpose of tsconfig.json
How TypeScript becomes JavaScript

By the end, you'll understand not just how TypeScript works, but why developers around the world rely on it every day.

What is TypeScript?

TypeScript is an open-source programming language developed by Microsoft.

The simplest way to understand TypeScript is this:

TypeScript is JavaScript with an additional type system.

That means every valid JavaScript program is also a valid TypeScript program.

For example, this JavaScript code works perfectly in TypeScript:

function greet(name) {
return "Hello " + name;
}

console.log(greet("Shivam"));

TypeScript doesn't force you to rewrite everything. Instead, it lets you gradually add types whenever you need them.

Here's the same code written in TypeScript:

function greet(name: string): string {
return Hello ${name};
}

console.log(greet("Shivam"));

Notice the difference?

name: string

and

): string

These tell TypeScript:

The parameter name must be a string.
The function will always return a string.

These simple additions allow TypeScript to detect mistakes before your application even runs.

JavaScript vs TypeScript

Let's compare them side by side.

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

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

Output:

30
1020

The second call isn't an error.

JavaScript simply converts the number into a string and concatenates them.

Sometimes this is useful.

Sometimes it's a nightmare.

Now look at TypeScript.

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

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

Immediately, your editor shows:

Argument of type 'string'
is not assignable to parameter of type 'number'.

The mistake is caught before your code runs.

This is one of the biggest reasons developers love TypeScript.

JavaScript Workflow
Developer


Write JavaScript


Browser Runs Code


Runtime Error (maybe)

If something is wrong, you'll only know after executing the application.

TypeScript Workflow
Developer


Write TypeScript


TypeScript Compiler

├── Errors Found ❌

└── JavaScript Generated ✅


Browser Executes

Instead of discovering problems at runtime, you fix them during development.

Why TypeScript Exists

To understand why TypeScript exists, let's first understand the problems JavaScript faces in large applications.

JavaScript is an amazing language. It's flexible, easy to learn, and runs almost everywhere—from browsers to servers.

However, flexibility comes with trade-offs.

Problem 1: Dynamic Typing

JavaScript variables can change their type at any time.

let age = 20;

age = "Twenty";

age = true;

age = {};

JavaScript allows all of this without any warnings.

While this flexibility is convenient, it can also lead to confusing bugs.

Imagine a function that calculates someone's birth year.

function getBirthYear(age) {
return 2026 - age;
}

console.log(getBirthYear(20));

Works perfectly.

But someone accidentally calls:

getBirthYear("Twenty");

The result?

NaN

The error isn't discovered until the function actually executes.

In TypeScript:

function getBirthYear(age: number): number {
return 2026 - age;
}

getBirthYear("Twenty");

Error:

Argument of type 'string'
is not assignable to parameter of type 'number'.

The bug never reaches production.

Problem 2: Runtime Errors

One of the biggest issues in JavaScript is that many mistakes are only discovered while the application is running.

Example:

const user = {
name: "Shivam"
};

console.log(user.email.toLowerCase());

The application crashes.

Cannot read property 'toLowerCase' of undefined

The code looked fine.

But the object didn't contain an email property.

Now let's define the structure using TypeScript.

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

const user: User = {
name: "Shivam"
};

TypeScript immediately reports:

Property 'email'
is missing.

Instead of discovering the issue after deployment, you fix it while writing the code.

Compile-Time Errors vs Runtime Errors

Understanding this difference is one of the most important concepts in TypeScript.

Compile-Time Error

A compile-time error is detected before the program runs.

Example:

let age: number = "Twenty";

TypeScript immediately stops you.

Type 'string'
is not assignable to type 'number'
Runtime Error

A runtime error occurs after the application starts.

Example:

const numbers = null;

console.log(numbers.length);

Output:

Cannot read property 'length' of null

The browser discovers the problem only after executing the code.

Visual Comparison
JavaScript

Write Code


Run Program


Error Appears
TypeScript

Write Code


Compiler Checks

├── Error Found


Fix Code


Run Program

TypeScript shifts many common errors from runtime to compile time, saving debugging time and improving reliability.

Benefits of Static Typing

Static typing is the core feature that makes TypeScript so valuable.

When you specify the type of your variables, functions, and objects, TypeScript can understand your code better and provide useful feedback.

Here are some key benefits:

  1. Fewer Bugs

TypeScript catches many mistakes before your application runs.

let price: number = 999;

price = "999";

Error immediately.

  1. Better Autocomplete

Because TypeScript knows the structure of your objects, editors like VS Code can provide intelligent suggestions.

const user = {
name: "Shivam",
age: 20,
city: "Mumbai"
};

user.

The editor instantly suggests:

name
age
city

No need to remember every property.

  1. Easier Refactoring

Imagine changing a property name in a project with 500 files.

Without TypeScript, you might accidentally miss several references.

With TypeScript, every broken reference is highlighted automatically.

  1. Better Team Collaboration

When another developer reads this function:

function createOrder(order: Order): Promise

They immediately understand:

What goes in
What comes out
Which data structure is expected

The code becomes self-documenting.

  1. Improved Maintainability

Large projects often stay active for years.

Developers join and leave.

Features are added constantly.

Strong typing helps ensure that changes in one part of the application don't unintentionally break another.

TypeScript Is a Superset of JavaScript

A common misconception is that TypeScript replaces JavaScript.

It doesn't.

Instead, TypeScript builds on top of JavaScript.

Think of JavaScript as the foundation and TypeScript as an extra safety layer.

      TypeScript
┌───────────────────┐
│  Types            │
│  Interfaces       │
│  Generics         │
│  Enums            │
│  Compiler Checks  │
└───────────────────┘
        ▲
        │
  JavaScript
Enter fullscreen mode Exit fullscreen mode

Every valid JavaScript program can be renamed from .js to .ts and still work. You can then gradually introduce TypeScript features as your project grows.

Why Modern Companies Choose TypeScript

Large applications demand reliability, maintainability, and developer productivity. That's why TypeScript has become the standard choice for modern web development.

Some key reasons include:

Early error detection before deployment
Better IDE support with autocomplete and refactoring
Improved code readability and self-documentation
Safer collaboration across large teams
Easier maintenance of long-term projects
Excellent integration with frameworks like React, Next.js, Angular, and Node.js

As projects grow in size and complexity, these advantages save countless hours of debugging and reduce the likelihood of production bugs.

In the next part, we'll dive into Type Annotations—learning how to add types to variables, functions, objects, arrays, and how TypeScript's powerful type inference works. We'll also compare explicit and inferred types with practical examples before moving on to Interfaces and Type Aliases.

next
Understanding Type Annotations

Now that we know why TypeScript exists, it's time to learn how TypeScript actually understands our code.

The biggest feature of TypeScript is its type system.

When we tell TypeScript what kind of data a variable should store, it can warn us whenever we accidentally use the wrong type.

This process is called Type Annotation.

What is a Type Annotation?

A type annotation is simply a way of telling TypeScript the expected type of a variable, parameter, or return value.

The syntax is very simple:

variableName: Type

For example,

let username: string = "Shivam";

Here,

username is the variable.
string is its type.
"Shivam" is the value.

Now TypeScript knows that username should always contain a string.

If we try to assign another type, TypeScript immediately reports an error.

username = 20;
Type 'number' is not assignable to type 'string'

Instead of waiting until the application runs, the mistake is caught while we're writing the code.

Why Are Type Annotations Important?

Imagine you're building an e-commerce application.

Each product has a price.

let price = 999;

Months later another developer accidentally writes

price = "999";

JavaScript happily accepts it.

Later your discount calculation breaks because it expected a number.

With TypeScript:

let price: number = 999;

price = "999";

Immediately:

Type 'string' is not assignable to type 'number'

A bug that could have reached production is stopped instantly.

Common Primitive Types

TypeScript provides several built-in types.

string

Stores text.

let firstName: string = "Shivam";

let city: string = "Mumbai";

let course: string = "Web Development";
number

Stores integers and decimal values.

let age: number = 20;

let salary: number = 50000;

let temperature: number = 35.6;

Unlike some languages, JavaScript and TypeScript have only one number type.

There is no separate int, float, or double.

boolean

Represents true or false.

let isLoggedIn: boolean = true;

let isAdmin: boolean = false;

Very commonly used for authentication and feature flags.

bigint

For very large integers.

let views: bigint = 12345678901234567890n;
symbol

Used to create unique identifiers.

let id: symbol = Symbol("id");

Most beginners won't use this often, but it's part of the language.

Arrays

An array stores multiple values of the same type.

JavaScript
const numbers = [1,2,3,4];

JavaScript doesn't enforce that future values remain numbers.

You could accidentally do:

numbers.push("Hello");

Now your array contains mixed data.

TypeScript
const numbers: number[] = [1,2,3,4];

Trying to push a string:

numbers.push("Hello");

Produces:

Argument of type 'string'
is not assignable to parameter of type 'number'

Another syntax:

const names: Array = [
"Shivam",
"Rahul",
"Ankit"
];

Both are correct.

Most developers prefer

string[]

because it's shorter.

Objects

Objects become much safer in TypeScript.

JavaScript:

const user = {
name: "Shivam",
age: 20
};

Nothing prevents someone from writing

user.age = "Twenty";

TypeScript:

const user: {
name: string;
age: number;
} = {
name: "Shivam",
age: 20
};

Now

user.age = "Twenty";

gives an error.

Functions

Functions are where TypeScript becomes incredibly useful.

We can specify

parameter types
return types
JavaScript
function greet(name){
return "Hello " + name;
}

No one knows what type name should be.

TypeScript
function greet(name: string): string {
return Hello ${name};
}

Let's understand this carefully.

name: string

means

The parameter must be a string.

): string

means

This function always returns a string.

Calling the function correctly

greet("Shivam");

works perfectly.

But

greet(10);

shows

Argument of type 'number'
is not assignable to parameter of type 'string'
Multiple Parameters
function add(
a: number,
b: number
): number {

return a + b;
Enter fullscreen mode Exit fullscreen mode

}

Usage

add(10,20);

Wrong

add("10",20);

Error immediately.

Optional Parameters

Sometimes a value isn't always available.

For example,

function greet(name: string, city?: string) {

if(city){
    return `Hello ${name} from ${city}`;
}

return `Hello ${name}`;
Enter fullscreen mode Exit fullscreen mode

}

Now both calls are valid.

greet("Shivam");

greet("Shivam","Mumbai");

The ? means the parameter is optional.

Default Parameters
function greet(
name: string,
city: string = "Mumbai"
){

return `Hello ${name} from ${city}`;
Enter fullscreen mode Exit fullscreen mode

}

Now

greet("Shivam");

returns

Hello Shivam from Mumbai
Void Return Type

Some functions don't return anything.

function printMessage(message: string): void {

console.log(message);
Enter fullscreen mode Exit fullscreen mode

}

The return type is

void

meaning

This function performs an action but returns nothing.

Never Return Type

The never type is used for functions that never successfully finish.

Example:

function throwError(message: string): never {

throw new Error(message);
Enter fullscreen mode Exit fullscreen mode

}

Because an error is thrown, the function never reaches the end.

Any Type

Sometimes we don't know the type.

TypeScript provides

any
let value: any;

value = 10;

value = "Hello";

value = true;

value = {};

This behaves almost exactly like JavaScript.

However,

avoid using any whenever possible.

Using any disables TypeScript's safety checks.

Think of it as turning off your seatbelt.

Unknown Type

Instead of any, TypeScript introduced unknown.

let value: unknown;

Unlike any, you cannot use it directly.

value.toUpperCase();

Error.

You must first check its type.

if(typeof value === "string"){

console.log(value.toUpperCase());
Enter fullscreen mode Exit fullscreen mode

}

This makes your code much safer.

Type Inference

One of TypeScript's smartest features is Type Inference.

Sometimes you don't even have to write the type.

Example

let age = 20;

Notice we didn't write

let age: number = 20;

Yet TypeScript already knows

age → number

If you later write

age = "Twenty";

You still get an error.

TypeScript automatically inferred the type.

Another example

const city = "Mumbai";

TypeScript infers

city: string

Functions also benefit from inference.

function multiply(a: number,b: number){

return a*b;
Enter fullscreen mode Exit fullscreen mode

}

We didn't write the return type.

TypeScript automatically infers

number
Explicit Types vs Inferred Types

There are two ways to define types.

Explicit

You manually specify the type.

let username: string = "Shivam";

Everything is written clearly.

Inferred

TypeScript figures it out.

let username = "Shivam";

Both produce the same result.

Which One Should You Use?

A common question is:

Should I always write types?

The answer is no.

Use inference whenever the type is obvious.

Good example

const age = 20;

No need to write

const age: number = 20;

But when writing functions, APIs, exported variables, or complex objects, explicit types improve readability.

function createUser(
name: string,
age: number
): User {

}

Anyone reading the code immediately understands what is expected.

Best Practices
Let TypeScript Infer Simple Types

Good

const username = "Shivam";

Not necessary

const username: string = "Shivam";
Always Type Function Parameters
function login(email: string,password: string){

}

Avoid

function login(email,password){

}
Avoid any

Instead of

let response: any;

prefer

let response: unknown;

or define a proper interface.

Always Specify Return Types for Public Functions
function calculateTotal(): number{

}

This improves readability and prevents accidental changes.

Common Beginner Mistakes
Using any Everywhere
let data:any;

This removes almost every benefit of TypeScript.

Forgetting Function Return Types

Large projects become difficult to understand.

Mixing Types
let age:number=20;

age="Twenty";

TypeScript exists specifically to prevent mistakes like this.

Overusing Explicit Types

Writing

const city:string="Mumbai";

everywhere makes code unnecessarily verbose.

Trust TypeScript's inference when appropriate.

Summary

Type annotations are the foundation of TypeScript. They allow you to describe the shape of your data, making your code more predictable and easier to maintain. Combined with TypeScript's powerful type inference, they strike a balance between safety and developer productivity.

By understanding how to annotate variables, functions, arrays, and objects—and knowing when to rely on inference—you'll write cleaner, more reliable applications with fewer runtime surprises.

Top comments (0)