TypeScript - Generics: A Beginner's Guide

Hello there, future coding superstar! Today, we're going to embark on an exciting journey into the world of TypeScript Generics. Don't worry if you're new to programming – I'll be your friendly guide, and we'll take this step by step. By the end of this tutorial, you'll be wielding generics like a pro!

TypeScript - Generics

What Are Generics and Why Should We Care?

Before we dive into the nitty-gritty, let's start with a simple analogy. Imagine you have a magic box that can hold any type of item. Sometimes you put in a book, sometimes a toy, or even a sandwich. That's essentially what generics are in TypeScript – they allow us to create flexible, reusable code that can work with different types.

Problem Examples

Let's look at a few scenarios where generics can save the day:

  1. You want to create a function that can reverse any type of array (numbers, strings, objects).
  2. You need a class that can store and retrieve any type of data.
  3. You're building a utility function that should work with various data types.

Without generics, you'd have to write separate functions or classes for each data type. That's a lot of repetition, and as any good programmer knows, repetition is the enemy of clean code!

TypeScript Generics to the Rescue!

Now, let's roll up our sleeves and see how generics work in action.

Basic Generic Function

Here's a simple generic function that can work with any type:

function identity<T>(arg: T): T {
    return arg;
}

Let's break this down:

  • <T> is our type parameter. It's like a placeholder for the type we'll use.
  • (arg: T) means our function takes an argument of type T.
  • : T after the parentheses means our function will return a value of type T.

We can use this function like so:

let output1 = identity<string>("Hello, Generics!");
let output2 = identity<number>(42);

console.log(output1); // "Hello, Generics!"
console.log(output2); // 42

Cool, right? The same function works with different types!

Generic Interface

We can also use generics with interfaces. Here's an example:

interface GenericBox<T> {
    contents: T;
}

let stringBox: GenericBox<string> = { contents: "A secret message" };
let numberBox: GenericBox<number> = { contents: 123 };

console.log(stringBox.contents); // "A secret message"
console.log(numberBox.contents); // 123

Our GenericBox can hold any type of content. It's like that magic box we talked about earlier!

Generic Classes

Let's create a generic class that can work as a simple data store:

class DataStore<T> {
    private data: T[] = [];

    addItem(item: T): void {
        this.data.push(item);
    }

    getItems(): T[] {
        return this.data;
    }
}

let stringStore = new DataStore<string>();
stringStore.addItem("Hello");
stringStore.addItem("World");
console.log(stringStore.getItems()); // ["Hello", "World"]

let numberStore = new DataStore<number>();
numberStore.addItem(1);
numberStore.addItem(2);
console.log(numberStore.getItems()); // [1, 2]

This DataStore class can store and retrieve any type of data. Pretty handy, huh?

Generic Constraints

Sometimes, we want to restrict the types that can be used with our generics. We can do this with constraints:

interface Lengthy {
    length: number;
}

function logLength<T extends Lengthy>(arg: T): void {
    console.log(arg.length);
}

logLength("Hello"); // 5
logLength([1, 2, 3]); // 3
logLength({ length: 10 }); // 10
// logLength(123); // Error: Number doesn't have a length property

Here, our logLength function can only work with types that have a length property.

Benefits of Generics

Now that we've seen generics in action, let's summarize their benefits:

  1. Code Reusability: Write once, use with many types.
  2. Type Safety: Catch type-related errors at compile-time.
  3. Flexibility: Create components that can work with a variety of data types.
  4. Clarity: Make the relationships between inputs and outputs clear.

Generic Methods Table

Here's a handy table of some common generic methods you might encounter:

Method Description Example
Array.map<U>() Transforms array elements [1, 2, 3].map<string>(n => n.toString())
Promise.all<T>() Waits for all promises to resolve Promise.all<number>([Promise.resolve(1), Promise.resolve(2)])
Object.keys<T>() Gets object keys as an array Object.keys<{name: string}>({name: "Alice"})
JSON.parse<T>() Parses JSON string to object JSON.parse<{age: number}>('{"age": 30}')

Conclusion

Congratulations! You've just taken your first steps into the wonderful world of TypeScript Generics. Remember, like any powerful tool, generics might feel a bit tricky at first, but with practice, they'll become second nature.

As you continue your coding journey, you'll find that generics are like a Swiss Army knife in your TypeScript toolbox – versatile, powerful, and incredibly useful. So go forth and code, young padawan, and may the generics be with you!

Happy coding, and until next time, keep exploring and learning!

Credits: Image by storyset