Practical examples of generics
Basic Concepts of Generics
Generics are a powerful type tool in TypeScript that allow us to create reusable components that can support multiple types without losing type safety. Generics work by not pre-specifying the concrete type when defining functions, interfaces, or classes, but instead specifying the type when they are used.
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("hello"); // Type is string
let output2 = identity<number>(42); // Type is number
Application of Generics in Functions
Generic functions are the most common use case for generics. They can handle different types of data without the need to write similar functions repeatedly.
function reverse<T>(items: T[]): T[] {
return items.reverse();
}
const numbers = [1, 2, 3];
const reversedNumbers = reverse(numbers); // number[]
const strings = ["a", "b", "c"];
const reversedStrings = reverse(strings); // string[]
Combining Interfaces with Generics
Generic interfaces can define flexible data structures that work with multiple data types.
interface KeyValuePair<K, V> {
key: K;
value: V;
}
let pair1: KeyValuePair<number, string> = { key: 1, value: "one" };
let pair2: KeyValuePair<string, boolean> = { key: "isActive", value: true };
Implementing Generics in Classes
Generic classes are particularly useful for creating reusable components, such as collection classes.
class Queue<T> {
private data: T[] = [];
push(item: T) {
this.data.push(item);
}
pop(): T | undefined {
return this.data.shift();
}
}
const numberQueue = new Queue<number>();
numberQueue.push(1);
numberQueue.push(2);
const stringQueue = new Queue<string>();
stringQueue.push("first");
stringQueue.push("second");
Generic Constraints
Sometimes we need to limit the range of types for generics, which can be achieved using generic constraints.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity("hello"); // Valid, strings have a length property
loggingIdentity([1, 2, 3]); // Valid, arrays have a length property
loggingIdentity(3); // Error, numbers do not have a length property
Generics with Default Types
TypeScript allows specifying default types for generic parameters, which can be useful in certain scenarios.
interface PaginatedResponse<T = any> {
data: T[];
total: number;
page: number;
}
const userResponse: PaginatedResponse<{ id: number; name: string }> = {
data: [{ id: 1, name: "Alice" }],
total: 1,
page: 1
};
const defaultResponse: PaginatedResponse = {
data: [1, 2, 3], // Uses the default any type
total: 3,
page: 1
};
Conditional Types and Generics
TypeScript's conditional types allow selecting types based on conditional expressions, which can be combined with generics to create powerful type utilities.
type NonNullable<T> = T extends null | undefined ? never : T;
type StringOrNumber = string | number | null;
type ValidType = NonNullable<StringOrNumber>; // string | number
Generics in Mapped Types
Mapped types can create new types based on old types, and their flexibility is enhanced when combined with generics.
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface User {
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>;
type PartialUser = Partial<User>;
Examples of Utility Types
TypeScript includes some built-in utility types based on generics that simplify development.
// Pick: Selects a set of properties K from type T
type UserPreview = Pick<User, "name">;
// Omit: Excludes a set of properties K from type T
type UserWithoutAge = Omit<User, "age">;
// Record: Constructs a type with property keys K and property values T
type UserMap = Record<string, User>;
Application of Generics in React Components
In React development, generics can be used to create type-safe components.
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <div>{items.map(renderItem)}</div>;
}
// Usage example
<List<number>
items={[1, 2, 3]}
renderItem={(item) => <div key={item}>{item}</div>}
/>
Advanced Generic Patterns
More complex generic patterns can solve specific type problems.
// Get the return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Get the parameters of a constructor
type ConstructorParameters<T> = T extends new (...args: infer P) => any ? P : never;
// Convert a tuple to a union type
type TupleToUnion<T extends any[]> = T[number];
type Example = TupleToUnion<[string, number, boolean]>; // string | number | boolean
Generics and Asynchronous Programming
Generics are particularly useful when handling asynchronous operations, ensuring types are correctly propagated through Promise chains.
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
interface UserData {
id: number;
name: string;
}
async function getUser() {
const user = await fetchData<UserData>("/api/user");
console.log(user.name); // Type-safe access
}
Generics and Higher-Order Functions
Generics can be used to create type-safe higher-order functions.
function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, ReturnType<T>>();
return function(...args: Parameters<T>): ReturnType<T> {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key)!;
}
const result = fn(...args);
cache.set(key, result);
return result;
} as T;
}
const add = (a: number, b: number) => a + b;
const memoizedAdd = memoize(add);
Generics and Type Inference
TypeScript's type inference system can automatically infer generic parameters, reducing redundant code.
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
// No need to explicitly specify type parameters
const result = merge({ name: "Alice" }, { age: 30 });
// result is automatically inferred as { name: string } & { age: number }
Generics and Function Overloading
Generics can be combined with function overloading to provide more precise type definitions.
function process<T extends string | number>(input: T): T extends string ? number : string;
function process(input: any): any {
return typeof input === "string" ? input.length : input.toString();
}
const strResult = process("hello"); // number
const numResult = process(42); // string
Generics and Index Types
Index type queries and index access operators can be used with generics.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
const age = getProperty(user, "age"); // number
Generics and Type Guards
Custom type guards can be combined with generics to provide safer type checking.
function isArrayOf<T>(arr: any[], typeGuard: (item: any) => item is T): arr is T[] {
return arr.every(typeGuard);
}
function isString(item: any): item is string {
return typeof item === "string";
}
const data: unknown = ["a", "b", "c"];
if (isArrayOf(data, isString)) {
// Inside this block, data is inferred as string[]
data.forEach(s => console.log(s.toUpperCase()));
}
Generics and Variadic Tuple Types
Variadic tuple types introduced in TypeScript 4.0 can be combined with generics to create more flexible function signatures.
function concat<T extends unknown[], U extends unknown[]>(
arr1: [...T],
arr2: [...U]
): [...T, ...U] {
return [...arr1, ...arr2];
}
const result = concat([1, 2], ["a", "b"]);
// result's type is [number, number, string, string]
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn