Published on: 10/06/2024
Written by James Bridge
Generics are a powerful feature in TypeScript that allow you to write flexible, reusable code while maintaining type safety. They enable you to create components that can work with a variety of types rather than a single one. Let’s dive into generics with some concrete examples.
Let’s start with a simple generic function:
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("myString"); // type of output1 is 'string'
let output2 = identity<number>(100); // type of output2 is 'number'
Here, T
is a type variable that gets replaced with an actual type when the function is called. TypeScript can also infer the type:
let output3 = identity("myString"); // TypeScript infers the type as string
Generics can be used with interfaces to create reusable component structures:
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
Classes can also leverage generics:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
constructor(zeroValue: T, addFn: (x: T, y: T) => T) {
this.zeroValue = zeroValue;
this.add = addFn;
}
}
let stringNumeric = new GenericNumber<string>('', (x, y) => x + y);
console.log(stringNumeric.add('Hello ', 'World')); // Outputs: Hello World
let numberNumeric = new GenericNumber<number>(0, (x, y) => x + y);
console.log(numberNumeric.add(5, 10)); // Outputs: 15
Sometimes you want to limit the types that can be used with a generic. You can do this using constraints:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
loggingIdentity({length: 10, value: 3}); // This works
// loggingIdentity(3); // This would be an error
You can declare a type parameter that is constrained by another type parameter:
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
console.log(getProperty(x, "a")); // Okay
// console.log(getProperty(x, "m")); // Error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
Here’s an example of a function that works with arrays of any type:
function reverseArray<T>(array: T[]): T[] {
return array.reverse();
}
const numbers = [1, 2, 3, 4, 5];
const reversedNumbers = reverseArray(numbers);
console.log(reversedNumbers); // Outputs: [5, 4, 3, 2, 1]
const strings = ["a", "b", "c", "d"];
const reversedStrings = reverseArray(strings);
console.log(reversedStrings); // Outputs: ["d", "c", "b", "a"]
Generics are particularly useful when working with Promises:
function wrapInPromise<T>(value: T): Promise<T> {
return new Promise<T>((resolve) => {
setTimeout(() => {
resolve(value);
}, 1000);
});
}
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
interface User {
id: number;
name: string;
}
// Usage
wrapInPromise("Hello, Generics!")
.then(result => console.log(result)); // Outputs after 1 second: Hello, Generics!
fetchData<User>("https://api.example.com/user/1")
.then(user => console.log(user.name));
If you’re using TypeScript with React, you can create generic components:
interface Props<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: Props<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage
interface User {
id: number;
name: string;
}
const users: User[] = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
function App() {
return (
<List<User>
items={users}
renderItem={(user) => user.name}
/>
);
}
These examples demonstrate how generics in TypeScript provide flexibility while maintaining type safety. They’re particularly useful for creating reusable components, functions, and data structures that can work with multiple types.