Generics Basics <T>
| Since: | TypeScript 1.0(2014) |
|---|
A feature that accepts types as arguments, allowing you to reuse the same logic for a variety of types. You can apply generics to functions, interfaces, and classes to write flexible, reusable code while keeping type checking active.
Syntax
function functionName<T>(arg: T): T {
return arg;
}
// Arrow function syntax
const functionName = <T>(arg: T): T => arg;
// Explicitly specifying the type argument when calling
functionName<string>("value");
// Type argument inference: you can omit the type if it can be determined from the argument.
functionName("value"); // T is inferred as string.
Syntax reference
| Syntax | Description |
|---|---|
| <T> | Declares a type parameter. Write it enclosed in <> after the function or type name. By convention, T is commonly used, but any name is allowed. |
| function f<T>(arg: T): T | The basic form of a generic function. Links the parameter type and return type using the same type parameter. |
| Type argument inference | Automatically determines the type parameter from the argument at the call site. Explicit specification is often unnecessary. |
| Multiple type parameters | You can define multiple type parameters, such as <T, U>. |
Sample code
function identity<T>(value: T): T {
return value;
}
// Calling with an explicit type argument.
const str = identity<string>("hello"); // type is string
const num = identity<number>(42); // type is number
// The type can be omitted thanks to type inference.
const autoStr = identity("TypeScript"); // T is inferred as string.
const autoNum = identity(100); // T is inferred as number.
console.log(autoStr); // Outputs "TypeScript".
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
console.log(first([1, 2, 3])); // Outputs 1.
console.log(first(["a", "b", "c"])); // Outputs "a".
console.log(first([])); // Outputs undefined.
// Using multiple type parameters.
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result = pair("item_x", 250); // type is [string, number].
console.log(result); // Outputs ["item_x", 250].
// A generic function that concatenates two arrays.
function merge<T>(arr1: T[], arr2: T[]): T[] {
return [...arr1, ...arr2];
}
const merged = merge([1, 2], [3, 4]); // type is number[].
console.log(merged); // Outputs [1, 2, 3, 4].
// A stack (last-in, first-out) implementation using generics.
function createStack<T>() {
const items: T[] = [];
return {
push: (item: T) => items.push(item),
pop: (): T | undefined => items.pop(),
peek: (): T | undefined => items[items.length - 1],
size: () => items.length,
};
}
const stack = createStack<number>();
stack.push(1);
stack.push(2);
console.log(stack.pop()); // Outputs 2.
Running the above produces the following output:
npx ts-node sample_ts_generics_basic.ts TypeScript 1 a undefined [ 'item_x', 250 ] 2
Constraints (extends) integration
Adding extends to a type parameter restricts which types are accepted. Constraints let you inform the compiler that a specific property exists on the type parameter.
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
console.log(getLength("Hello, World!")); // 13
console.log(getLength([1, 2, 3])); // 3
// console.log(getLength(42)); // Compile error (number has no length)
// Combined with keyof: safely retrieve an object property.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const product = { name: "item_x", price: 250, category: "electronics" };
console.log(getProperty(product, "name")); // item_x
console.log(getProperty(product, "price")); // 250
// console.log(getProperty(product, "color")); // Compile error
// Default type parameters (TypeScript 2.3+)
interface Box<T = string> {
value: T;
}
const strBox: Box = { value: "config_data" }; // T defaults to string
const numBox: Box<number> = { value: 500 }; // T is explicitly number
console.log(strBox.value, numBox.value); // config_data 500
Running the above produces the following output:
npx ts-node sample_generics_extends.ts 13 3 item_x 250 config_data 500
Practical patterns
Common generic patterns used in real-world TypeScript projects.
interface ApiResponse<T> {
data: T | null;
error: string | null;
status: number;
}
interface User {
id: number;
name: string;
}
function createSuccess<T>(data: T): ApiResponse<T> {
return { data, error: null, status: 200 };
}
function createError<T>(message: string, status: number): ApiResponse<T> {
return { data: null, error: message, status };
}
const userResponse = createSuccess<User>({ id: 1, name: "member_1" });
const errResponse = createError<User>("Not Found", 404);
console.log(userResponse.data?.name); // member_1
console.log(errResponse.error); // Not Found
// Pattern 2: Type-safe event emitter
type EventMap = {
login: { userId: number; username: string };
logout: { userId: number };
};
function emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void {
console.log(`Event: ${event}`, payload);
}
emit("login", { userId: 1, username: "user_1" });
// emit("login", { userId: 1 }); // Compile error (username is missing)
Running the above produces the following output:
npx ts-node sample_generics_patterns.ts
member_1
Not Found
Event: login { userId: 1, username: 'user_1' }
Common Mistakes
Common mistake 1: using any
Using any disables type checking, and the type-tracking that generics provide no longer functions.
function badIdentity(value: any): any { return value; }
Fix: use generics to define a general-purpose function while keeping type checking active.
function goodIdentity<T>(value: T): T { return value; }
Common mistake 2: unconstrained property access
Accessing a property on a type parameter without a constraint causes a compile error. TypeScript does not know that T has a name property.
function getNameBad<T>(obj: T): string { return obj.name; }
Fix: add a constraint with extends.
function getNameGood<T extends { name: string }>(obj: T): string {
return obj.name;
}
Common mistake 3: omitting type arguments
Omitting the type argument causes T to become unknown, which leads to type errors.
class Repository<T> {
private items: T[] = [];
add(item: T): void { this.items.push(item); }
getAll(): T[] { return this.items; }
}
const repo = new Repository(); // T becomes unknown, leading to type errors
Fix: specify the type argument explicitly.
interface User { id: number; name: string; }
class Repository<T> {
private items: T[] = [];
add(item: T): void { this.items.push(item); }
getAll(): T[] { return this.items; }
}
const repo = new Repository<User>(); // Specify the type argument explicitly
repo.add({ id: 2, name: "member_2" });
console.log(repo.getAll()[0].name); // member_2
Running the above produces the following output:
npx ts-node sample_generics_mistakes.ts member_2
Overview
Generics let you treat types like variables. By using a type parameter (<T>), you can define functions and types whose actual type is determined at the call site. This lets you write a single function that works with both string arrays and number arrays.
By convention, type parameter names like T (Type), K (Key), V (Value), and E (Element) are commonly used, but you can choose any descriptive name you prefer. When you need multiple type parameters, separate them with commas, as in <T, U>.
TypeScript can usually infer the type parameter from the argument, so you can write identity("hello") instead of identity<string>("hello"). Specify the type argument explicitly only when inference does not work as expected or when you want to make your intent clear. To add constraints to a type parameter, see Generic constraints: extends.
If you find any errors or copyright issues, please contact us.