πŸ”€ Generics Fundamentals

TypeScript Fundamentals β€” Generics Fundamentals
TypeScript Fundamentals
Course 1 Β· Chapter 6 Β· Generics Fundamentals

πŸ”€ Generics Fundamentals

Generics are TypeScript's way of writing flexible, reusable code that works with many types while maintaining type safety. Imagine a function that works with numbers, strings, objectsβ€”any typeβ€”and still catches type errors. That's generics.

⚠️ The Problem Generics Solve

Without generics, you'd write the same function multiple times for different types, or resort to `any` and lose type safety.
// ❌ Without generics: lots of duplication function getFirstString(arr: string[]): string { return arr[0]; } function getFirstNumber(arr: number[]): number { return arr[0]; } function getFirstAny(arr: any[]): any { return arr[0]; } // Same logic, written 3 times. And if you need boolean, object, etc., write it again!
❌ With `any`, you lose type safety:
const str = getFirstAny(["hello", "world"]); // TypeScript thinks str is 'any', so: str.toUpperCase(); // βœ… Works, but TypeScript doesn't know if it's safe str.toReverse(); // ❌ No error, but this method doesn't exist!

πŸ“ Generic Functions

Your First Generic Function

function getFirst<T>(arr: T[]): T { return arr[0]; } // TypeScript infers the type based on usage: const firstString = getFirst(["hello", "world"]); // T is 'string' // firstString is type 'string', so: firstString.toUpperCase(); // βœ… TypeScript knows it's safe const firstNumber = getFirst([1, 2, 3]); // T is 'number' // firstNumber is type 'number', so: firstNumber.toFixed(2); // βœ… TypeScript knows it's safe
βœ… Key insight:

<T> is a type parameter β€” a placeholder for any type. When you call getFirst([1,2,3]), TypeScript sees the argument is number[], so it sets T = number. The function now behaves as if it were typed specifically for numbers.

Multiple Type Parameters

Functions can have multiple type parameters:

function pair<T, U>(first: T, second: U): [T, U] { return [first, second]; } // Different types for each parameter: const result = pair("hello", 42); // T is 'string', U is 'number' // result is ["hello", 42] with type [string, number]

πŸ›οΈ Generic Classes

Classes can be generic too. A common example is a container or wrapper:

class Box<T> { private value: T; constructor(initialValue: T) { this.value = initialValue; } getValue(): T { return this.value; } setValue(newValue: T): void { this.value = newValue; } } // Create a box for strings: const stringBox = new Box<string>("hello"); stringBox.getValue(); // Type: string // Create a box for numbers: const numberBox = new Box<number>(42); numberBox.getValue(); // Type: number

πŸ”’ Generic Constraints

Sometimes you want to restrict what types a generic can accept. For example, "only types that have a `length` property" or "only types that extend a specific interface."

Constraint: Must Have a Property

interface HasLength { length: number; } function getLength<T extends HasLength>(item: T): number { return item.length; } // βœ… Works: strings have length getLength("hello"); // Returns 5 // βœ… Works: arrays have length getLength([1, 2, 3]); // Returns 3 // ❌ Error: numbers don't have length getLength(42); // Error!

Constraint: Must Extend a Type

function merge<T extends object>(obj1: T, obj2: T): T { return { ...obj1, ...obj2 }; } // βœ… Works: objects can be merged merge({ a: 1 }, { b: 2 }); // ❌ Error: strings can't be merged this way merge("hello", "world");

Constraint: Generic Must Be a Key

Ensure a type parameter is a key of another type parameter:

function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } const person = { name: "Alice", age: 30 }; getProperty(person, "name"); // βœ… "name" is a key of person getProperty(person, "email"); // ❌ "email" is not a key of person

🎯 Common Generic Patterns

Array Methods

function map<T, U>(arr: T[], fn: (T) => U): U[] { return arr.map(fn); } // Transform strings to numbers: const lengths = map(["a", "bb", "ccc"], (s) => s.length); // Result: [1, 2, 3]

Promise Resolution

function fetchData<T>(url: string): Promise<T> { return fetch(url).then((res) => res.json()); } interface User { id: number; name: string; } // Promise resolves to User: const userPromise = fetchData<User>("/api/user"); // userPromise is Promise<User>

πŸ’» Coding Challenges

Challenge 1: Generic Array Reverse

Write a generic function that reverses an array of any type and returns the reversed array. The function should maintain type safety.

function reverse<T>(arr: T[]): T[] { // Your implementation }

Goal: Work with strings, numbers, objectsβ€”any type.

β†’ Solution

Challenge 2: Generic Cache Class

Create a generic Cache<T> class that stores a value and provides get/set methods. The value can be any type.

  • Constructor takes an initial value
  • get() method returns the cached value
  • set(value) method updates the cache

Goal: Demonstrate generic classes.

β†’ Solution

Challenge 3: Generic with Constraint

Write a generic function that accepts an object and a property name, then returns the property value. Use a constraint to ensure the property exists on the object.

function getProp<T, K>(obj: T, key: K) { // Your implementation }

Goal: Demonstrate generic constraints with `keyof`.

β†’ Solution

πŸ’‘ Real-World Tip: Generics Make Your Code DRY

If you find yourself writing the same function for different types, or using `any` to make it work with multiple types, generics are your answer. Libraries like React, Vue, Express, and database ORMs use generics extensively. Understanding them now will pay dividends as you read real-world code.

🎯 What's Next

Generics are powerful, but TypeScript has even more advanced type features. Chapter 7 covers Advanced Types β€” union types, type guards, discriminated unions, and more sophisticated patterns for keeping your code type-safe.