TypeScript Generics
Generics allow you to create reusable components that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types while maintaining type safety.
1. The Problem
Without generics, we would either have to give a specific type to a function or use any.
// Specific type: Not reusable for numbers
function identity(arg: string): string {
return arg;
}
// Any: We lose information about what that type was when the function returns
function identity(arg: any): any {
return arg;
}
2. Generic Syntax
We use a type variable, usually denoted as <T>, to capture the type the user provides (e.g., number or string).
function identity<T>(arg: T): T {
return arg;
}
Now we can call it in two ways:
// 1. Explicitly passing the type
let output = identity<string>("myString");
// 2. Type Argument Inference (Compiler figures it out)
let output2 = identity("myString");
3. Generic Interfaces
You can define interfaces that accept type arguments.
interface Box<T> {
contents: T;
}
let stringBox: Box<string> = { contents: "Hello" };
let numberBox: Box<number> = { contents: 42 };
4. Generic Classes
Classes can also be generic.
class KeyValuePair<K, V> {
constructor(public key: K, public value: V) {}
}
let pair = new KeyValuePair<number, string>(1, "a");
5. Generic Constraints
Sometimes you want to restrict the types that can be used with a generic. For example, if you want to access .length, you need to constrain T to types that have a .length property.
interface Lengthwise {
length: number;
}
// T must extend Lengthwise
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no error
return arg;
}
loggingIdentity({ length: 10, value: 3 }); // OK
// loggingIdentity(3); // Error, number doesn't have .length