TypeScript Generics
Generics are one of the most powerful features in TypeScript. They allow us to write reusable, flexible components (like functions, classes, or interfaces) that can work with multiple data types, not just a single one. This greatly improves code reusability while maintaining type safety.
Why Do We Need Generics?
Imagine a simple function that takes a parameter and returns it directly.
// Using any type, but loses type information
function identity_any(arg: any): any {
return arg;
}
// Using specific type, but cannot be reused
function identity_number(arg: number): number {
return arg;
}Using the any type causes the function to lose type safety because the compiler doesn't know the specific type of the return value. Writing a function for each type is very tedious. Generics were created to solve this problem.
Creating Generic Functions
We can create a generic function identity that works for all types.
function identity<T>(arg: T): T {
return arg;
}Here, <T> is a type variable, a special kind of variable used only to represent types, not values. It captures the type passed in by the user (like number), and then we can use this type inside the function.
Using Generic Functions
There are two ways to use generic functions:
Explicitly pass the type parameter:
typescriptlet output = identity<string>("myString"); // output's type is stringType inference: The compiler automatically determines the type of
Tbased on the passed argument.typescriptlet output = identity("myString"); // Compiler infers T as string, output's type is string let numOutput = identity(123); // Compiler infers T as number, numOutput's type is number
Generic Constraints
Sometimes, we want a generic function to work with types that have specific properties. For example, a logging function might need to access the .length property of the parameter. In this case, we can use generic constraints.
We can create an interface to describe the constraint condition, then use the extends keyword to apply it.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we can safely access the .length property
return arg;
}
loggingIdentity("hello"); // OK, string has length property
loggingIdentity([1, 2, 3]); // OK, array has length property
// loggingIdentity(123); // Error: number doesn't have length property
// loggingIdentity({ name: 'test' }); // Error: { name: string } doesn't have length propertyGeneric Interfaces
We can also create generic interfaces, allowing interface member types to be flexible.
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
console.log(myIdentity(100)); // 100
// console.log(myIdentity("test")); // ErrorGeneric Classes
Classes can also be generic. Generic classes use <T> after the class name to define type parameters.
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
// Generic class instance with string type
let myGenericString = new GenericNumber<string>();
myGenericString.zeroValue = "";
myGenericString.add = function(x, y) { return x + y; };
console.log(myGenericString.add("hello", " world")); // "hello world"
// Generic class instance with number type
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
console.log(myGenericNumber.add(10, 20)); // 30Generics are a very important part of TypeScript's type system. Mastering them allows you to write more general, robust, and reusable code.