TypeScript Basics: Why You Should Switch
If you've been writing JavaScript for any length of time, you've probably encountered the dreaded undefined is not a function or spent hours debugging why user.name is mysteriously undefined. These runtime errors are JavaScript's gift that keeps on giving—but TypeScript is here to change that.
TypeScript has rapidly become the industry standard for building large-scale JavaScript applications. Companies like Microsoft, Google, Airbnb, and Slack have all adopted TypeScript, and there's a good reason why. In this comprehensive guide, we'll explore what TypeScript is, why you should switch, and how to get started.
TypeScript catches errors before your code runs. Imagine having a helpful assistant that points out mistakes as you type, suggests completions, and ensures your code is consistent. That's TypeScript.
What is TypeScript?
TypeScript is a statically-typed superset of JavaScript developed by Microsoft. This means every valid JavaScript code is also valid TypeScript code—but TypeScript adds optional static typing, interfaces, and other features that help you write more robust and maintainable code.
Think of TypeScript as JavaScript with a safety net. It compiles down to plain JavaScript, so it runs anywhere JavaScript runs—browsers, Node.js, Deno, you name it.
TypeScript compiles to JavaScript while catching type errors during compilation
JavaScript vs TypeScript: A Quick Comparison
Let's see the difference between JavaScript and TypeScript with a simple example. Notice how TypeScript adds type annotations:
JavaScript
function greet(name) {
return "Hello, " + name;
}
// No error - but this is wrong!
greet(42);
greet({ name: "John" });
greet();
TypeScript
function greet(name: string): string {
return "Hello, " + name;
}
// ❌ Error: Argument of type
// 'number' is not assignable
greet(42);
// ✅ This works!
greet("John");
In JavaScript, the function happily accepts any value—a number, an object, or even nothing. You won't know there's a problem until runtime. TypeScript catches these issues instantly in your editor.
Why You Should Switch to TypeScript
Here are the compelling reasons why switching to TypeScript will make you a more productive developer:
Catch Bugs Before Runtime
TypeScript's static type checking catches errors during development, not when your users encounter them. Studies show TypeScript can prevent up to 15% of bugs that would otherwise make it to production.
Intelligent Autocomplete
Your editor becomes supercharged with accurate autocomplete suggestions, method signatures, and inline documentation. No more guessing what properties an object has.
Safer Refactoring
Rename a function or change a type, and TypeScript instantly shows you every place that needs updating. Large-scale refactoring becomes confident rather than scary.
Self-Documenting Code
Type definitions serve as inline documentation. New team members can understand your codebase faster because the types tell them exactly what data flows through each function.
Better Team Collaboration
Types create contracts between different parts of your code. When working in teams, this means fewer miscommunications about data shapes and API expectations.
Core TypeScript Concepts
Let's explore the fundamental building blocks of TypeScript. Understanding these concepts will set you up for success.
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
TypeScript provides several basic types that you'll use constantly:
// 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);
}
Using any defeats the purpose of TypeScript—you lose all type checking benefits. If you must use it, consider unknown instead, which requires you to narrow the type before using it.
Interfaces and Type Aliases
Interfaces and type aliases let you define custom types for objects and more:
// 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;
};
- Interfaces can be extended and merged (declaration merging)
- Type aliases are more flexible for unions, tuples, and primitives
- For object shapes, both work—use interface for public APIs
- Use type for unions, intersections, and computed types
Union and Intersection Types
TypeScript allows you to combine types in powerful ways:
// 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 allow you to write reusable code that works with multiple types while maintaining type safety:
// 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");
T— Type (most common)K— KeyV— ValueE— ElementR— Return type
Getting Started with TypeScript
Let's set up TypeScript in your project. It's easier than you might think!
Install TypeScript
Install TypeScript globally or as a dev dependency in your project
# Install globally
npm install -g typescript
# Or as dev dependency (recommended)
npm install --save-dev typescript
# Check version
tsc --version
Initialize Configuration
Create a tsconfig.json file to configure the TypeScript compiler
# Generate tsconfig.json with defaults
tsc --init
Configure tsconfig.json
Adjust the configuration for your project needs
{
"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"]
}
Write and Compile
Create TypeScript files and compile them to JavaScript
# 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
TypeScript integrates seamlessly with modern frameworks. Here's how to use it:
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
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
Always enable "strict": true in tsconfig.json. This turns on all strict type-checking options and catches more potential bugs.
// 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
TypeScript is smart! Don't add types when they're obvious. Let the compiler infer types for cleaner code.
// 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
TypeScript provides built-in utility types that make working with types easier:
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
const data: any = fetchData(); // Defeats TypeScript's purpose!
interface ApiData { id: number; name: string; }
const data: ApiData = await fetchData();
const user = users.find(u => u.id === id);
console.log(user.name); // Error if user is undefined!
const user = users.find(u => u.id === id);
if (user) { console.log(user.name); }
// Or: console.log(user?.name ?? "Unknown");
const input = document.getElementById("email") as HTMLInputElement;
input.value = "test"; // Might crash if element doesn't exist!
const input = document.getElementById("email");
if (input instanceof HTMLInputElement) {
input.value = "test"; // Safe!
}
Conclusion
TypeScript isn't just a trend—it's a fundamental improvement to how we write JavaScript. By adding static types, you get:
- Fewer bugs reaching production
- Better developer experience with intelligent tooling
- Self-documenting code that's easier to maintain
- Confidence when refactoring large codebases
- Improved team collaboration through type contracts
The learning curve is minimal if you already know JavaScript—TypeScript is just JavaScript with types. Start by adding it to a small project, enable strict mode, and let the compiler guide you. Within a week, you'll wonder how you ever coded without it.
You don't have to convert your entire codebase at once. TypeScript allows gradual adoption—start with new files, then slowly migrate existing JavaScript. Use allowJs: true to mix .js and .ts files.
Ready to make the switch? Your future self (and your team) will thank you. Happy typing! 🎉
