TypeScript Introduction

Type annotations, interfaces, generics, and why TypeScript matters

TypeScript Fundamentals

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. It adds optional static typing and class-based object-oriented programming. TypeScript helps catch errors at compile time and improves code quality and developer experience.

Why TypeScript?

  • Type Safety — Catch errors before runtime
  • Better IDE Support — Autocomplete, refactoring, navigation
  • Self-Documenting — Types serve as inline documentation
  • Refactoring — Safer large-scale code changes
  • Modern Features — ES6+ features with backward compatibility

Basic Types

// Primitive types
let name: string = "Alice";
let age: number = 25;
let isActive: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;

// Arrays
let numbers: number[] = [1, 2, 3];
let names: Array<string> = ["Alice", "Bob"];

// Tuples - fixed length arrays with specific types
let tuple: [string, number] = ["Alice", 25];

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

// Any - opt out of type checking (avoid when possible)
let data: any = "hello";
data = 42; // No error

// Unknown - type-safe any
let input: unknown = getUserInput();
if (typeof input === "string") {
  console.log(input.toUpperCase()); // Safe
}

// Void - no return value
function logMessage(message: string): void {
  console.log(message);
}

// Never - function never returns
function throwError(message: string): never {
  throw new Error(message);
}

Type Inference

// TypeScript infers types automatically
let message = "Hello"; // inferred as string
let count = 42;        // inferred as number
let items = [1, 2, 3]; // inferred as number[]

// Contextual typing
const names = ["Alice", "Bob", "Charlie"];
names.forEach(name => {
  // name is inferred as string
  console.log(name.toUpperCase());
});

// Best practice: Let TypeScript infer when obvious
let user = { name: "Alice", age: 25 };
// Explicit when needed for clarity
function calculateTotal(items: CartItem[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

Interfaces

// Define 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: "Alice",
  email: "alice@example.com",
  createdAt: new Date()
};

// Extending interfaces
interface AdminUser extends User {
  role: "admin";
  permissions: string[];
}

// Function interface
interface SearchFunction {
  (query: string, limit?: number): Promise<Result[]>;
}

// Index signatures
interface Dictionary {
  [key: string]: string;
}

const translations: Dictionary = {
  hello: "hola",
  goodbye: "adiós"
};

Type Aliases

// Create custom types
type ID = string | number;
type Point = { x: number; y: number };

// Union types
type Status = "pending" | "active" | "completed";
type Result = string | null;

// Intersection types
type Employee = Person & { employeeId: string };

// Function types
type Callback = (data: string) => void;
type AsyncHandler = (req: Request) => Promise<Response>;

// Template literal types
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = `/api/${string}`;

// Utility types
type UserPreview = Pick<User, "id" | "name">;
type UserWithoutEmail = Omit<User, "email">;
type ReadonlyUser = Readonly<User>;
type PartialUser = Partial<User>;
type RequiredUser = Required<User>;

Generics

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

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

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

const userResponse: ApiResponse<User> = {
  data: { id: 1, name: "Alice", email: "a@b.com", createdAt: new Date() },
  status: 200,
  message: "Success"
};

// Generic 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(123);   // Error - numbers don't have length

// Multiple type parameters
function pair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}

// Generic classes
class DataStore<T> {
  private items: T[] = [];
  
  add(item: T): void {
    this.items.push(item);
  }
  
  get(index: number): T | undefined {
    return this.items[index];
  }
  
  getAll(): T[] {
    return [...this.items];
  }
}

Type Guards

// Type narrowing with typeof
function process(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase();
  }
  return value.toFixed(2);
}

// instanceof guard
class Dog { bark() { console.log("Woof!"); } }
class Cat { meow() { console.log("Meow!"); } }

function speak(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.meow();
  }
}

// in operator guard
interface Fish { swim(): void; }
interface Bird { fly(): void; }

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim();
  } else {
    animal.fly();
  }
}

// Custom type guard
interface User { type: "user"; name: string; }
interface Admin { type: "admin"; permissions: string[]; }

function isAdmin(person: User | Admin): person is Admin {
  return person.type === "admin";
}

const person: User | Admin = getUser();
if (isAdmin(person)) {
  console.log(person.permissions); // TypeScript knows it's Admin
}

Interface vs Type Alias

Feature Interface Type Alias
Object shapes
Extends/Inheritance ✅ extends ✅ intersection (&)
Declaration merging
Union types
Primitives
Tuples

Use interfaces for object shapes, types for unions and primitives.

Working with Functions

// Function with typed parameters and return
function greet(name: string, greeting = "Hello"): string {
  return `${greeting}, ${name}!`;
}

// Optional and default parameters
function createUser(
  name: string,
  email: string,
  age?: number,        // Optional
  role = "user"        // Default value
): User {
  return { name, email, age, role };
}

// Rest parameters

  
function sum(...numbers: number[]): number {
  return numbers.reduce((a, b) => a + b, 0);
}

// Function overloads
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
  if (typeof value === "string") {
    return value.trim();
  }
  return value.toFixed(2);
}

// Arrow functions with generics
const fetchData = async <T>(url: string): Promise<T> => {
  const response = await fetch(url);
  return response.json();
};

💡 Getting Started

  • • Install: npm install -D typescript
  • • Initialize: npx tsc --init
  • • Use strict mode for maximum type safety
  • • Start by renaming .js files to .ts
  • • Add types incrementally, use 'any' sparingly
  • • Enable noImplicitAny to catch untyped code