Introduction to TypeScript for JavaScript Developers
Why TypeScript?
TypeScript adds static typing to JavaScript, catching errors before runtime and improving code quality, maintainability, and developer experience.
Benefits of TypeScript
Catch Errors Early:
// JavaScript - Error at runtime
function greet(name) {
return name.toUppercase(); // Typo!
}
// TypeScript - Error at compile time
function greet(name: string): string {
return name.toUppercase(); // Error: Property 'toUppercase' does not exist
}
Better IDE Support:
- Autocomplete
- Inline documentation
- Refactoring tools
- Go to definition
- Find all references
Self-Documenting Code:
// Clear what this function expects and returns
function calculateTax(amount: number, rate: number): number {
return amount * rate;
}
Easier Refactoring:
- Rename variables safely
- Change interfaces confidently
- Find all usages instantly
Setting Up TypeScript
Install TypeScript:
npm install -g typescript
Initialize Project:
mkdir my-ts-project
cd my-ts-project
npm init -y
npm install typescript --save-dev
tsc --init
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Compile TypeScript:
tsc # Compile once
tsc --watch # Watch mode
Basic Types
Primitive Types:
// String
let name: string = "John";
// Number
let age: number = 30;
// Boolean
let isActive: boolean = true;
// Null and Undefined
let nothing: null = null;
let notDefined: undefined = undefined;
// Any (avoid when possible)
let anything: any = "could be anything";
Arrays:
// Array of numbers
let numbers: number[] = [1, 2, 3];
let moreNumbers: Array<number> = [4, 5, 6];
// Array of strings
let names: string[] = ["John", "Jane"];
// Array of mixed types
let mixed: (number | string)[] = [1, "two", 3];
Tuples:
// Fixed-length array with specific types
let person: [string, number] = ["John", 30];
// Accessing elements
let personName = person[0]; // string
let personAge = person[1]; // number
Enums:
// Numeric enum
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
// String enum
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING",
}
let currentStatus: Status = Status.Active;
Interfaces and Types
Interfaces:
interface User {
id: number;
name: string;
email: string;
age?: number; // Optional property
readonly createdAt: Date; // Read-only property
}
const user: User = {
id: 1,
name: "John Doe",
email: "john@example.com",
createdAt: new Date(),
};
// user.createdAt = new Date(); // Error: Cannot assign to readonly
Type Aliases:
type ID = number | string;
type Point = {
x: number;
y: number;
};
let userId: ID = "abc123";
let coordinate: Point = { x: 10, y: 20 };
Extending Interfaces:
interface Animal {
name: string;
age: number;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
const myDog: Dog = {
name: "Buddy",
age: 3,
breed: "Golden Retriever",
bark() {
console.log("Woof!");
},
};
Functions
Typing Functions:
// Function declaration
function add(a: number, b: number): number {
return a + b;
}
// Arrow function
const multiply = (a: number, b: number): number => {
return a * b;
};
// Optional parameters
function greet(name: string, greeting?: string): string {
return `${greeting || "Hello"}, ${name}!`;
}
// Default parameters
function createUser(name: string, role: string = "user") {
return { name, role };
}
// Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
Function Types:
// Function type
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
Void and Never:
// Void: function returns nothing
function logMessage(message: string): void {
console.log(message);
}
// Never: function never returns
function throwError(message: string): never {
throw new Error(message);
}
Union and Intersection Types
Union Types:
// Can be one type OR another
type StringOrNumber = string | number;
function format(value: StringOrNumber): string {
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toFixed(2);
}
// Union with literal types
type Status = "success" | "error" | "pending";
function handleStatus(status: Status) {
// status can only be one of these three values
}
Intersection Types:
// Must have properties from ALL types
type Person = {
name: string;
age: number;
};
type Employee = {
employeeId: string;
department: string;
};
type StaffMember = Person & Employee;
const staff: StaffMember = {
name: "John",
age: 30,
employeeId: "E123",
department: "IT",
};
Generics
Generic Functions:
// Generic function
function identity<T>(value: T): T {
return value;
}
let num = identity<number>(42);
let str = identity<string>("hello");
// Type inference
let inferred = identity("world"); // T is inferred as string
Generic Interfaces:
interface Box<T> {
value: T;
}
let numberBox: Box<number> = { value: 42 };
let stringBox: Box<string> = { value: "hello" };
Generic Classes:
class DataStore<T> {
private data: T[] = [];
add(item: T): void {
this.data.push(item);
}
get(index: number): T | undefined {
return this.data[index];
}
getAll(): T[] {
return this.data;
}
}
const numberStore = new DataStore<number>();
numberStore.add(1);
numberStore.add(2);
const stringStore = new DataStore<string>();
stringStore.add("hello");
stringStore.add("world");
Classes
Basic Class:
class Person {
// Properties
name: string;
private age: number;
protected email: string;
// Constructor
constructor(name: string, age: number, email: string) {
this.name = name;
this.age = age;
this.email = email;
}
// Method
greet(): string {
return `Hello, I'm ${this.name}`;
}
// Getter
get info(): string {
return `${this.name} (${this.age})`;
}
// Setter
set updateAge(age: number) {
if (age > 0) {
this.age = age;
}
}
}
Shorthand Constructor:
class User {
constructor(
public name: string,
private email: string,
protected age: number,
) {}
}
Inheritance:
class Animal {
constructor(public name: string) {}
move(distance: number): void {
console.log(`${this.name} moved ${distance}m`);
}
}
class Dog extends Animal {
constructor(
name: string,
public breed: string,
) {
super(name);
}
bark(): void {
console.log("Woof! Woof!");
}
}
const dog = new Dog("Buddy", "Golden Retriever");
dog.move(10);
dog.bark();
Abstract Classes:
abstract class Shape {
abstract area(): number;
abstract perimeter(): number;
describe(): string {
return `Area: ${this.area()}, Perimeter: ${this.perimeter()}`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
perimeter(): number {
return 2 * Math.PI * this.radius;
}
}
Type Guards
typeof:
function process(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toFixed(2);
}
instanceof:
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
Custom Type Guards:
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
}
Utility Types
Partial:
interface User {
id: number;
name: string;
email: string;
}
// All properties optional
type PartialUser = Partial<User>;
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}
Required:
interface Config {
host?: string;
port?: number;
}
// All properties required
type RequiredConfig = Required<Config>;
Pick:
type UserPreview = Pick<User, "id" | "name">;
Omit:
type UserWithoutEmail = Omit<User, "email">;
Record:
type PageInfo = {
title: string;
url: string;
};
type Pages = Record<"home" | "about" | "contact", PageInfo>;
Migrating JavaScript to TypeScript
Step 1: Rename Files
.js→.ts.jsx→.tsx
Step 2: Add tsconfig.json Start with loose settings:
{
"compilerOptions": {
"strict": false,
"noImplicitAny": false
}
}
Step 3: Gradually Add Types Start with function parameters and return types
Step 4: Enable Strict Mode Once most code is typed:
{
"compilerOptions": {
"strict": true
}
}
Best Practices
-
Enable Strict Mode
"strict": true -
Avoid
anyUseunknownwhen type is truly unknown -
Use Interfaces for Objects
interface User { ... } -
Leverage Type Inference
// Good: TypeScript infers number let count = 5; // Unnecessary: explicit type let count: number = 5; -
Use Union Types Over Enums
type Status = "active" | "inactive" | "pending"; -
Make Illegal States Unrepresentable
// Bad interface User { loading: boolean; error: string | null; data: UserData | null; } // Good type User = | { status: "loading" } | { status: "error"; error: string } | { status: "success"; data: UserData };
Conclusion
TypeScript improves JavaScript development with type safety, better tooling, and self-documenting code. Start small, gradually add types, and embrace the compiler as your friend. The initial investment in learning TypeScript pays dividends in reduced bugs and improved maintainability.