Working with the DOM
Learn how to use TypeScript to interact with the DOM and create dynamic web pages.
Generics in TypeScript: A Beginner's Guide
What are Generics?
Imagine you're writing a function that needs to work with different types of data, like numbers, strings, or even custom objects. You could write separate functions for each type, but that's repetitive and inefficient. Generics provide a way to write code that's reusable across multiple types *without* sacrificing type safety.
Think of generics as placeholders for types. You define these placeholders when you create a function, interface, or class. When you *use* that function, interface, or class, you specify the actual type that the placeholder should represent. TypeScript then makes sure everything is type-safe based on that specific type.
In essence, generics allow you to write code that's both flexible and type-safe. It promotes code reuse and avoids the need for `any` type, which sacrifices type checking.
Why Use Generics?
- Code Reusability: Write code once that works with different data types.
- Type Safety: TypeScript enforces type checking based on the specific type you provide for the generic placeholder, preventing runtime errors.
- Avoid `any`: Using `any` disables type checking. Generics allow you to be type-safe without resorting to `any`.
- Better Performance: No need for runtime type checks or casting, leading to potentially better performance compared to using `any`.
Example: A Simple Generic Function
Let's say we want to write a function that takes an array and returns the first element. Without generics, we might have to use `any` or write multiple functions.
function getFirstElement(arr: any[]): any {
return arr[0];
}
const numbers = [1, 2, 3];
const firstNumber = getFirstElement(numbers); // firstNumber is of type any
const strings = ["hello", "world"];
const firstString = getFirstElement(strings); // firstString is of type any
Notice that `firstNumber` and `firstString` are both typed as `any`. We've lost type information.
Now, let's use a generic:
function getFirstElement<T>(arr: T[]): T | undefined {
return arr.length > 0 ? arr[0] : undefined;
}
const numbers = [1, 2, 3];
const firstNumber = getFirstElement(numbers); // firstNumber is of type number
const strings = ["hello", "world"];
const firstString = getFirstElement(strings); // firstString is of type string
const emptyArray: number[] = [];
const firstOfEmpty = getFirstElement(emptyArray); // firstOfEmpty is number | undefined
Here's what's happening:
- `<T>` declares `T` as a type variable. It's a placeholder for a type. You can name it anything you want (e.g., `<DataType>`, `<ItemType>`), but `T` is a common convention.
- `arr: T[]` means the `arr` argument must be an array of type `T`.
- `T | undefined` declares the return type. In cases where the array is empty the function will return undefined.
- When we call `getFirstElement(numbers)`, TypeScript infers that `T` is `number` because `numbers` is an array of numbers. Therefore, the return type is `number`.
- Similarly, when we call `getFirstElement(strings)`, TypeScript infers that `T` is `string`, and the return type is `string`.
We've retained type safety without writing separate functions for numbers and strings!
You can also explicitly specify the type parameter:
const explicitNumber = getFirstElement<number>([4, 5, 6]); // explicitNumber is of type number
// Example of potential error if you force the type
// const explicitNumberError = getFirstElement<string>([7, 8, 9]); // Type 'number' is not assignable to type 'string'.
Creating Reusable Components Using Generics
Generics are especially powerful when building reusable components, particularly in frameworks like React or Angular.
Example: A Generic List Component (Conceptual)
Let's imagine we're building a UI component that displays a list of items. We want this component to work with lists of numbers, strings, objects, or any other data type.
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode; // Assuming React for this example
}
function ListComponent<T>(props: ListProps<T>) {
return (
<ul>
{props.items.map((item, index) => (
<li key={index}>{props.renderItem(item)}</li>
))}
</ul>
);
}
// Usage with numbers:
const numberItems = [10, 20, 30];
const NumberList = () => <ListComponent items={numberItems} renderItem={(num) => <div>Number: {num}</div>} />;
// Usage with strings:
const stringItems = ["apple", "banana", "cherry"];
const StringList = () => <ListComponent items={stringItems} renderItem={(str) => <div>Fruit: {str}</div>} />;
// Usage with custom objects:
interface Person { name: string; age: number; }
const people: Person[] = [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }];
const PeopleList = () => <ListComponent items={people} renderItem={(person) => <div>Name: {person.name}, Age: {person.age}</div>} />;
Key Points:
- We define an interface `ListProps<T>` with a generic type `T`.
- `items: T[]` specifies that the `items` prop is an array of type `T`.
- `renderItem: (item: T) => React.ReactNode` is a function prop that takes an item of type `T` and returns a React node (how to render each item).
- The `ListComponent<T>` function uses the generic type `T` to define the type of the `props`.
- When we use `ListComponent`, TypeScript infers the type of `T` based on the `items` prop we provide.
This approach allows us to reuse the `ListComponent` for different types of data without having to write separate components for each type.
Generic Constraints
Sometimes, you want to restrict the types that can be used with a generic. You can do this using generic constraints.
For example, let's say you want to write a function that can only work with objects that have a `name` property.
interface Named {
name: string;
}
function printName<T extends Named>(obj: T): void {
console.log(obj.name);
}
const validObject = { name: "John", age: 30 };
printName(validObject); // Works fine
//const invalidObject = { age: 30 };
//printName(invalidObject); // Error: Argument of type '{ age: number; }' is not assignable to parameter of type 'Named'.
Here, `<T extends Named>` means that `T` must be a type that is assignable to `Named`. In other words, `T` must have at least the properties defined in the `Named` interface (in this case, a `name` property of type `string`).
Conclusion
Generics are a powerful feature in TypeScript that can significantly improve the reusability, type safety, and maintainability of your code. While they might seem a bit confusing at first, understanding the basic concepts and examples presented here will set you on the right path to mastering generics and writing more robust TypeScript applications.