Generics and function overloading
Basic Concepts of Generics
Generics are a core tool in TypeScript for creating reusable components. They allow developers to write code that can handle multiple types without repeating logic for each type. The essence of generics is type parameterization, where functions, interfaces, or classes are defined without specifying concrete types in advance, but instead specify them when used.
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("hello");
let output2 = identity<number>(42);
In this example, T
is a type variable that captures the type passed by the user. When calling, the type parameter can be explicitly specified or inferred by TypeScript:
let output3 = identity("world"); // Inferred as string
let output4 = identity(100); // Inferred as number
Generic Constraints
Sometimes we need to restrict the range of generic parameter types. In such cases, the extends
keyword can be used to add 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(3); // Error, numbers lack a length property
Multiple type parameters can also be combined:
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2 };
getProperty(x, "a"); // Valid
getProperty(x, "c"); // Error, "c" is not a property of x
Generic Interfaces and Classes
Generics can also be applied to interfaces and classes:
interface GenericIdentityFn<T> {
(arg: T): T;
}
let myIdentity: GenericIdentityFn<number> = identity;
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = (x, y) => x + y;
Basics of Function Overloading
Function overloading allows a function to accept different types or numbers of arguments and return different types of results. In TypeScript, function overloading is implemented by providing multiple function signatures:
function reverse(value: string): string;
function reverse(value: number): number;
function reverse(value: string | number): string | number {
if (typeof value === "string") {
return value.split("").reverse().join("");
}
return Number(value.toString().split("").reverse().join(""));
}
reverse("hello"); // Returns "olleh"
reverse(123); // Returns 321
Combining Overloading with Generics
Generics and function overloading can be combined to create more flexible type-safe functions:
function combine<T>(a: T, b: T): T;
function combine<T, U>(a: T, b: U): [T, U];
function combine(a: any, b: any) {
if (typeof a === typeof b) {
return a + b;
}
return [a, b];
}
combine(1, 2); // Returns 3
combine("a", "b"); // Returns "ab"
combine(1, "a"); // Returns [1, "a"]
Advanced Overloading Patterns
For more complex scenarios, conditional types and mapped types can be used to enhance overloading:
type OverloadedReturn<T> =
T extends string ? number :
T extends number ? string :
never;
function transform<T extends string | number>(input: T): OverloadedReturn<T>;
function transform(input: any) {
if (typeof input === "string") {
return input.length;
}
return input.toString();
}
const strLength = transform("hello"); // Type number
const numStr = transform(42); // Type string
Practical Application Scenarios
Generics and overloading are particularly useful when handling API responses:
interface ApiResponse<T> {
data: T;
status: number;
}
function fetchData<T>(url: string): Promise<ApiResponse<T>>;
function fetchData<T>(url: string, callback: (data: T) => void): void;
function fetchData<T>(url: string, callback?: (data: T) => void): Promise<ApiResponse<T>> | void {
const promise = fetch(url).then(res => res.json());
if (callback) {
promise.then(data => callback(data));
return;
}
return promise;
}
// Usage 1: Promise-based
fetchData<User>("/api/user").then(response => {
console.log(response.data);
});
// Usage 2: Callback-based
fetchData<Product>("/api/product", product => {
console.log(product.price);
});
Performance Considerations
While generics and overloading provide strong type safety, the impact on compile-time performance should also be considered. Complex generic types and multiple overloads can increase type-checking time. For performance-sensitive projects, consider:
- Avoiding overly nested generic types
- Limiting the number of overload signatures (typically no more than 5-7)
- Using simpler type annotations in hot-path code
// Complex generics that may impact performance
type DeepNested<T> = {
[K in keyof T]: T[K] extends object ? DeepNested<T[K]> : T[K];
};
// Simpler alternative
interface Simplified {
id: string;
name: string;
metadata?: Record<string, any>;
}
Boundaries of Type Inference
TypeScript's type inference has clear boundaries when handling generics and overloading. Understanding these limitations helps in writing more robust code:
function firstElement<T>(arr: T[]): T {
return arr[0];
}
// Automatically inferred as unknown[]
const result = firstElement([]);
// Explicit type parameter needed
const betterResult = firstElement<string>([]);
For overloaded functions, TypeScript tries each overload signature in order until a match is found:
function createDate(timestamp: number): Date;
function createDate(year: number, month: number, day: number): Date;
function createDate(overload1: number, overload2?: number, overload3?: number): Date {
if (overload2 !== undefined && overload3 !== undefined) {
return new Date(overload1, overload2, overload3);
}
return new Date(overload1);
}
createDate(1640995200000); // Uses the first overload
createDate(2022, 0, 1); // Uses the second overload
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn