← Back to Course

🔮 Type Inference & Basic Generics

TypeScript is smart! It can often figure out types automatically through type inference. Generics allow you to write flexible, reusable code that works with multiple types while maintaining type safety.

Type Inference

TypeScript automatically infers types based on values and context:

Example: Automatic Type Inference

// TypeScript infers type from initial value
let message = "Hello TypeLand";  // inferred as string
let count = 42;                  // inferred as number
let isActive = true;             // inferred as boolean

// Type inference with arrays
let numbers = [1, 2, 3];         // inferred as number[]
let mixed = [1, "two", true];    // inferred as (string | number | boolean)[]

// Type inference with objects
let player = {
  name: "Hero",
  level: 5
};  // inferred as { name: string; level: number; }

// Function return type inference
function add(a: number, b: number) {
  return a + b;  // Return type inferred as number
}

const result = add(5, 3);  // result inferred as number

When to Add Explicit Types

While inference is powerful, explicit types improve readability and catch errors earlier:

Example: Explicit vs Inferred

// Good: Inferred type is obvious
let age = 25;

// Better: Explicit type for clarity and safety
let userAge: number;  // Declared but not initialized
userAge = 30;

// Good: Function parameters MUST be annotated
function greet(name: string) {
  return `Hello, ${name}`;
}

// Better: Explicit return type for documentation
function greet(name: string): string {
  return `Hello, ${name}`;
}

// Variables that will change type context
let data;  // type: any (no inference)
data = "string";
data = 123;  // No error with 'any'

// Better: Specify expected type
let data: string;
data = "string";
// data = 123;  // ✗ Error!

Introduction to Generics

Generics let you write code that works with multiple types while preserving type information:

Example: Basic Generic Function

// Generic function - works with any type
function identity<T>(value: T): T {
  return value;
}

// TypeScript infers the type parameter
const num = identity(42);        // T is number
const str = identity("hello");   // T is string
const bool = identity(true);     // T is boolean

// Or specify explicitly
const result = identity<string>("TypeScript");

// Without generics, you'd need:
function identityNumber(value: number): number { return value; }
function identityString(value: string): string { return value; }
// ... repetitive!

Generic Arrays

Arrays already use generics behind the scenes:

Example: Generic Array Functions

// Generic function that works with any array type
function getFirstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

const numbers = [1, 2, 3];
const first = getFirstElement(numbers);  // type: number | undefined

const names = ["Alice", "Bob"];
const firstName = getFirstElement(names);  // type: string | undefined

// Generic function to reverse an array
function reverseArray<T>(arr: T[]): T[] {
  return arr.slice().reverse();
}

const reversed = reverseArray([1, 2, 3]);  // [3, 2, 1], type: number[]

Generic Interfaces

Interfaces can also be generic:

Example: Generic Interfaces

// Generic interface
interface Box<T> {
  value: T;
}

// Use with different types
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "hello" };
const playerBox: Box<{ name: string; level: number }> = {
  value: { name: "Hero", level: 5 }
};

// Generic interface with methods
interface Container<T> {
  items: T[];
  add(item: T): void;
  remove(item: T): boolean;
  find(predicate: (item: T) => boolean): T | undefined;
}

// Implementing generic interface
class Inventory<T> implements Container<T> {
  items: T[] = [];
  
  add(item: T): void {
    this.items.push(item);
  }
  
  remove(item: T): boolean {
    const index = this.items.indexOf(item);
    if (index > -1) {
      this.items.splice(index, 1);
      return true;
    }
    return false;
  }
  
  find(predicate: (item: T) => boolean): T | undefined {
    return this.items.find(predicate);
  }
}

const stringInventory = new Inventory<string>();
stringInventory.add("sword");
stringInventory.add("shield");

Practical Use Cases

Example: Real-World Generics

// API response wrapper
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

type User = { id: number; name: string; };
type Product = { id: number; title: string; price: number; };

function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  // Fetch implementation
  return fetch(url).then(res => res.json());
}

// Type-safe API calls
const userResponse = fetchData<User>("/api/user/1");
const productResponse = fetchData<Product>("/api/product/5");

// Key-value storage
class Storage<T> {
  private items = new Map<string, T>();
  
  set(key: string, value: T): void {
    this.items.set(key, value);
  }
  
  get(key: string): T | undefined {
    return this.items.get(key);
  }
}

const stringStorage = new Storage<string>();
stringStorage.set("name", "Alice");
const name = stringStorage.get("name");  // string | undefined

🎯 Key Concepts

  • Type inference automatically determines types from context
  • Always annotate function parameters - they cannot be inferred
  • Explicit types improve code documentation and catch errors earlier
  • Generics use <T> to work with any type
  • Generics preserve type information through transformations
  • Common naming: T (Type), K (Key), V (Value), E (Element)
  • Array<T> and Promise<T> are built-in generic types
← Back to Beginner Course