TS
Frontend

TypeScript Basics: Why You Should Switch

Mayur Dabhi
Mayur Dabhi
February 27, 2026
18 min read

You rename a field from userName to name in your API response, fix the three places you remember, and ship. Two days later a bug report comes in: the profile page shows "Hello, undefined". Or you get TypeError: Cannot read properties of undefined (reading 'map') in production because an endpoint returned null instead of an empty array. Plain JavaScript lets all of this through until someone actually runs the code.

TypeScript moves a big chunk of those mistakes from "a user found it" to "my editor underlined it". This post covers what TypeScript actually adds on top of JavaScript, the core types you'll use every day, how to set it up in a project, and the mistakes I see most often when people start using it.

The Short Version

TypeScript checks your code before it runs. You get red squiggles for wrong arguments, missing properties and possible undefined values while you type, plus autocomplete that knows the shape of your data.

What is TypeScript?

Take your JavaScript, add type annotations where you want them, and you have TypeScript. Microsoft maintains it, and it's a superset of JavaScript: any valid JavaScript is also valid TypeScript. On top of that you get optional static types, interfaces, generics and a compiler that checks them.

The types only exist at build time. The compiler strips them out and emits plain JavaScript, so the result runs anywhere JavaScript runs: browsers, Node.js, Deno, Bun.

TypeScript in Your Workflow .ts / .tsx TypeScript Source Code TypeScript Compiler (tsc) .js JavaScript Output Type Checking Errors caught here!

TypeScript compiles to JavaScript while catching type errors during compilation

JavaScript vs TypeScript: A Quick Comparison

Here's the same function in both languages. The only change is the type annotations:

JavaScript

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

// No error - but this is wrong!
greet(42);
greet({ name: "John" });
greet();
VS

TypeScript

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

// ❌ Error: Argument of type 
// 'number' is not assignable
greet(42);

// ✅ This works!
greet("John");

The JavaScript version accepts anything: a number, an object, or no argument at all. You find out when the output looks wrong. The TypeScript version refuses to compile the bad call, and your editor shows the error as soon as you type it.

Why You Should Switch to TypeScript

These are the things that actually change day to day once a project is typed:

Catch Bugs Before Runtime

Type errors show up while you're writing the code, not when a user hits them. A 2017 academic study ("To Type or Not to Type", Gao, Bird and Barr) looked at real bugs in public JavaScript projects and found that type annotations would have caught roughly 15% of them. That's not everything, but it's a lot of bugs you never have to debug.

Autocomplete That Knows Your Data

Your editor can suggest the real properties of an object, show function signatures and pull in doc comments. You stop opening three files to remember whether it was user.id or user.userId.

Safer Refactoring

Rename a property or change a function signature and the compiler lists every place that breaks. That's exactly the "renamed field" bug from the intro, caught before you commit.

Self-Documenting Code

A function signature tells you what goes in and what comes out, and unlike a comment it can't go stale without the build failing. New people on a project can read the types instead of guessing.

Better Team Collaboration

Types act as contracts between parts of the code. If the backend and frontend share a User type, there's far less back and forth about which fields an endpoint returns.

Core TypeScript Concepts

You can get a long way with just these six ideas:

Basic Types

string, number, boolean, null, undefined, any

Arrays & Tuples

Typed arrays and fixed-length tuples

Interfaces

Define object shapes and contracts

Type Aliases

Create custom type names

Union Types

One of several types

Generics

Reusable typed components

Basic Types

These are the types you'll write constantly:

basic-types.ts
// Primitive types
let username: string = "john_doe";
let age: number = 25;
let isActive: boolean = true;

// Arrays
let scores: number[] = [85, 90, 78];
let names: Array<string> = ["Alice", "Bob"];

// Tuple (fixed-length array with specific types)
let user: [string, number] = ["John", 30];

// Enum
enum Status {
  Pending = "PENDING",
  Active = "ACTIVE",
  Completed = "COMPLETED"
}
let orderStatus: Status = Status.Active;

// Any (escape hatch - use sparingly!)
let flexible: any = "hello";
flexible = 42; // No error

// Unknown (safer than any)
let uncertain: unknown = "hello";
// uncertain.toUpperCase(); // Error! Must narrow first
if (typeof uncertain === "string") {
  uncertain.toUpperCase(); // OK!
}

// Void (for functions that don't return)
function logMessage(msg: string): void {
  console.log(msg);
}

// Never (for functions that never return)
function throwError(message: string): never {
  throw new Error(message);
}
Avoid 'any' When Possible

Once a value is any, the compiler stops checking it, and anything you derive from it is unchecked too. If you really don't know the type, use unknown: it forces you to narrow the type before you use the value.

Interfaces and Type Aliases

For anything bigger than a primitive, you'll name the shape with an interface or a type alias:

interfaces.ts
// Interface - defines object shape
interface User {
  id: number;
  name: string;
  email: string;
  age?: number;        // Optional property
  readonly createdAt: Date;  // Cannot be modified
}

// Using the interface
const user: User = {
  id: 1,
  name: "John Doe",
  email: "john@example.com",
  createdAt: new Date()
};

// Type alias - can define any type
type ID = string | number;
type Status = "pending" | "active" | "completed";

// Type alias for object (similar to interface)
type Product = {
  id: ID;
  name: string;
  price: number;
  status: Status;
};

// Extending interfaces
interface Employee extends User {
  department: string;
  salary: number;
}

// Intersection types (combine types)
type Manager = Employee & {
  team: string[];
  level: number;
};
Interface vs Type Alias
  • Interfaces can be extended and merged (declaration merging)
  • Type aliases are more flexible for unions, tuples, and primitives
  • For object shapes, both work. I use interface for public APIs
  • Use type for unions, intersections, and computed types

Union and Intersection Types

A union says "one of these", an intersection says "all of these at once":

Union vs Intersection Types Union Type (A | B) A B "Can be A OR B" Intersection Type (A & B) A B "Must be A AND B"
union-intersection.ts
// Union Type: value can be one of several types
type StringOrNumber = string | number;

function printId(id: StringOrNumber) {
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // TypeScript knows it's string here
  } else {
    console.log(id.toFixed(2)); // TypeScript knows it's number here
  }
}

printId("abc123"); // OK
printId(42);       // OK

// Literal Union Types
type Direction = "north" | "south" | "east" | "west";
type HttpStatus = 200 | 201 | 400 | 404 | 500;

function move(direction: Direction) {
  console.log(`Moving ${direction}`);
}

move("north"); // OK
// move("up");  // Error: not in union

// Intersection Type: combines multiple types
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge;

const person: Person = {
  name: "John",
  age: 30
  // Must have BOTH properties
};

Generics

Generics let one function or class work with many types without falling back to any. You'll use them most for things like API response wrappers and collections:

generics.ts
// Generic function
function identity<T>(value: T): T {
  return value;
}

const str = identity("hello");   // type: string
const num = identity(42);        // type: number

// Generic with constraints
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): void {
  console.log(item.length);
}

logLength("hello");      // OK - strings have length
logLength([1, 2, 3]);    // OK - arrays have length
// logLength(42);        // Error - numbers don't have length

// Generic interface
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

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

const userResponse: ApiResponse<User> = {
  data: { id: 1, name: "John" },
  status: 200,
  message: "Success"
};

// Generic class
class DataStore<T> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  get(index: number): T {
    return this.items[index];
  }

  getAll(): T[] {
    return [...this.items];
  }
}

const numberStore = new DataStore<number>();
numberStore.add(1);
numberStore.add(2);

const stringStore = new DataStore<string>();
stringStore.add("hello");
stringStore.add("world");
Common Generic Type Parameters
  • T: Type (most common)
  • K: Key
  • V: Value
  • E: Element
  • R: Return type

Getting Started with TypeScript

Setting it up in a plain Node project takes four steps. Frameworks usually do most of this for you (see the next section).

1

Install TypeScript

Install TypeScript globally or as a dev dependency in your project

Terminal
# Install globally
npm install -g typescript

# Or as dev dependency (recommended)
npm install --save-dev typescript

# Check version
tsc --version
2

Initialize Configuration

Create a tsconfig.json file to configure the TypeScript compiler

Terminal
# Generate tsconfig.json with defaults
tsc --init
3

Configure tsconfig.json

Adjust the configuration for your project needs

tsconfig.json
{
  "compilerOptions": {
    // Target JavaScript version
    "target": "ES2020",
    
    // Module system
    "module": "ESNext",
    "moduleResolution": "node",
    
    // Output directory
    "outDir": "./dist",
    "rootDir": "./src",
    
    // Strict type checking (recommended!)
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    
    // Additional checks
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    
    // Allow JavaScript files
    "allowJs": true,
    
    // Source maps for debugging
    "sourceMap": true,
    
    // Faster builds
    "skipLibCheck": true,
    
    // Interop with CommonJS
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
4

Write and Compile

Create TypeScript files and compile them to JavaScript

Terminal
# Compile once
tsc

# Watch mode (recompile on changes)
tsc --watch

# Or use package.json scripts
npm run build   # tsc
npm run dev     # tsc --watch

TypeScript with Popular Frameworks

Most current frameworks support TypeScript out of the box, so you rarely write the config by hand:

React with TypeScript

# Create new React app with TypeScript
npx create-react-app my-app --template typescript

# Add TypeScript to existing React app
npm install --save-dev typescript @types/react @types/react-dom

Heads up: Create React App was officially deprecated in early 2025. For a new project I'd use npm create vite@latest my-app -- --template react-ts or a framework like Next.js instead.

Example Component:

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
}

const Button: React.FC<ButtonProps> = ({ label, onClick, disabled }) => {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
};

Next.js with TypeScript

# Create new Next.js app with TypeScript
npx create-next-app@latest my-app --typescript

# Add TypeScript to existing Next.js app
touch tsconfig.json
npm install --save-dev typescript @types/react @types/node
npm run dev  # Next.js auto-configures TypeScript

Example Page:

import { GetServerSideProps, NextPage } from 'next';

interface Props {
  users: User[];
}

const UsersPage: NextPage<Props> = ({ users }) => {
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
};

export const getServerSideProps: GetServerSideProps<Props> = async () => {
  const users = await fetchUsers();
  return { props: { users } };
};

Node.js with TypeScript

# Initialize and install
npm init -y
npm install --save-dev typescript @types/node ts-node nodemon

# Create tsconfig.json
npx tsc --init

Example Express Server:

import express, { Request, Response } from 'express';

const app = express();

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

app.get('/users/:id', (req: Request, res: Response) => {
  const userId = parseInt(req.params.id);
  const user: User = { id: userId, name: 'John' };
  res.json(user);
});

app.listen(3000, () => console.log('Server running'));

JavaScript vs TypeScript Comparison

Feature JavaScript TypeScript
Type System Dynamic (runtime) Static (compile-time)
Error Detection At runtime During development
IDE Support Basic Excellent (autocomplete, refactoring)
Learning Curve Lower Slightly higher
Code Documentation Manual (JSDoc) Built-in via types
Refactoring Safety Risky Safe with compiler checks
Build Step Optional Required (compilation)
Ecosystem Huge Huge (same as JS + @types)

TypeScript Best Practices

1. Enable Strict Mode

Turn on "strict": true in tsconfig.json from day one. It enables the whole family of strict checks, including strictNullChecks, which is where most of the real bugs get caught. Turning it on later in a big codebase means fixing hundreds of errors at once.

// tsconfig.json
{
  "compilerOptions": {
    "strict": true  // Enables all strict checks
  }
}

2. Prefer 'unknown' Over 'any'

When you don't know the type, use unknown instead of any. It forces you to perform type checking before using the value.

// Bad
function processData(data: any) {
  return data.toUpperCase(); // No error, but might crash!
}

// Good
function processData(data: unknown) {
  if (typeof data === "string") {
    return data.toUpperCase(); // Safe!
  }
  throw new Error("Expected string");
}

3. Use Type Inference

You don't need to annotate everything. The compiler infers types for variables just fine, so save annotations for function parameters and return types, where they act as the contract.

// Verbose (unnecessary)
const name: string = "John";
const numbers: number[] = [1, 2, 3];

// Clean (TypeScript infers the types)
const name = "John";           // inferred as string
const numbers = [1, 2, 3];     // inferred as number[]

// DO add types for function parameters and return types
function add(a: number, b: number): number {
  return a + b;
}

4. Use Utility Types

Before writing a near-copy of an existing type, check whether a built-in utility type already does it:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// Partial - makes all properties optional
type UserUpdate = Partial<User>;

// Pick - select specific properties
type UserPreview = Pick<User, "id" | "name">;

// Omit - exclude specific properties
type SafeUser = Omit<User, "password">;

// Required - makes all properties required
type RequiredUser = Required<Partial<User>>;

// Readonly - makes all properties readonly
type FrozenUser = Readonly<User>;

// Record - create object type with specific keys
type UserRoles = Record<"admin" | "user" | "guest", string[]>;

Common TypeScript Mistakes to Avoid

❌ Mistake: Using 'any' everywhere const data: any = fetchData(); // Defeats TypeScript's purpose!
✅ Better: Define proper types interface ApiData { id: number; name: string; }
const data: ApiData = await fetchData();
❌ Mistake: Ignoring null/undefined const user = users.find(u => u.id === id);
console.log(user.name); // Error if user is undefined!
✅ Better: Handle nullable types const user = users.find(u => u.id === id);
if (user) { console.log(user.name); }
// Or: console.log(user?.name ?? "Unknown");
❌ Mistake: Type assertions without checking const input = document.getElementById("email") as HTMLInputElement;
input.value = "test"; // Might crash if element doesn't exist!
✅ Better: Use type guards const input = document.getElementById("email");
if (input instanceof HTMLInputElement) {
input.value = "test"; // Safe!
}

Two errors you'll hit in your first week, usually right after installing a package:

When I'd Use TypeScript (and When I Wouldn't)

For anything that more than one person touches, or that I'll still be maintaining in six months, I reach for TypeScript by default. That covers most React and Next.js frontends, Node APIs and shared libraries. The bigger the codebase, the more the refactoring safety pays off.

I'd skip it, or hold off, in a few cases:

Also be honest about the costs: there's a compile step, CI gets slower on large projects, and some advanced generic types are hard to read. None of that outweighs the benefits for a real application, but it's not free.

Migrating an Existing Project

You don't have to convert everything at once. Set allowJs: true so .js and .ts files can live side by side, write new files in TypeScript, and convert old ones as you touch them. Start with the files that model your data (API clients, shared types), because everything else benefits from those first.

TypeScript JavaScript Types Static Typing Frontend
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building web applications with Laravel, React, Next.js and TypeScript.