Generics and Inheritance
Basic Concepts of Generics
Generics are a core tool in TypeScript for creating reusable components. They allow defining functions, interfaces, or classes without specifying the exact type in advance, instead specifying it at the time of use. The core idea of generics is type parameterization, where types are passed as parameters.
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("hello");
let output2 = identity<number>(42);
Generics declare type parameters using the <T>
syntax, where T
can be used within the function body. Types can be explicitly specified during invocation or inferred automatically by TypeScript. The main advantage of generics is providing code reusability while maintaining type safety.
Basic Concepts of Inheritance
Inheritance is an important feature of object-oriented programming, and TypeScript implements class inheritance using the extends
keyword. Subclasses can inherit properties and methods from parent classes while adding their own features or overriding parent class behavior.
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distance: number = 0) {
console.log(`${this.name} moved ${distance}m`);
}
}
class Dog extends Animal {
bark() {
console.log("Woof! Woof!");
}
}
const dog = new Dog("Buddy");
dog.bark(); // Woof! Woof!
dog.move(10); // Buddy moved 10m
Combining Generics and Inheritance
When generics meet inheritance, a more flexible type system can be created. Generic classes can inherit from non-generic classes, non-generic classes can inherit from generic classes, and even generic classes can inherit from one another.
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
class StringNumber extends GenericNumber<string> {
constructor() {
super();
this.zeroValue = "";
this.add = (x, y) => x + y;
}
}
const stringNum = new StringNumber();
console.log(stringNum.add("Hello", "World")); // HelloWorld
Generic Constraints and Inheritance
The extends
keyword can be used to constrain generic parameters, limiting them to conform to a specific type. This is particularly useful when accessing specific properties or methods.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity("hello"); // 5
loggingIdentity([1, 2, 3]); // 3
loggingIdentity({length: 10, value: "test"}); // 10
Type Parameters in Generic Class Inheritance
When generic classes inherit, subclasses can retain the parent class's type parameters or fix some or all of them.
class Base<T, U> {
constructor(public prop1: T, public prop2: U) {}
}
class Derived1<V> extends Base<string, V> {
constructor(prop2: V) {
super("default", prop2);
}
}
class Derived2 extends Base<number, boolean> {
constructor(prop1: number, prop2: boolean) {
super(prop1, prop2);
}
}
Method Overriding and Generics
Subclasses can override generic methods from parent classes but must maintain compatible signatures. The return type can be a subtype of the parent method's return type.
class Parent {
process<T>(input: T): T[] {
return [input];
}
}
class Child extends Parent {
process<T extends string>(input: T): string[] {
return [input.toUpperCase()];
}
}
const child = new Child();
console.log(child.process("hello")); // ["HELLO"]
Generic Interface Inheritance
Interfaces can also use generics and support inheritance. Generic interfaces can inherit from non-generic interfaces, and vice versa.
interface NotGeneric {
id: number;
}
interface Generic<T> extends NotGeneric {
value: T;
}
interface Pair<T, U> {
first: T;
second: U;
}
interface NumberPair extends Pair<number, number> {
sum(): number;
}
const pair: NumberPair = {
first: 1,
second: 2,
sum() { return this.first + this.second; }
};
Conditional Types and Inheritance
TypeScript 2.8 introduced conditional types, which, combined with the extends
keyword, allow selecting different types based on type relationships.
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
type ExtractString<T> = T extends string ? T : never;
type C = ExtractString<"hello" | 42 | true>; // "hello"
Generics and Class Static Members
Static members cannot directly use a class's type parameters, but similar functionality can be achieved through generic functions or static generic methods.
class GenericClass<T> {
static defaultValue: any; // Cannot be T
static create<U>(value: U): GenericClass<U> {
const instance = new GenericClass<U>();
// Initialization logic
return instance;
}
}
const instance = GenericClass.create<string>("test");
Advanced Pattern: Mixins and Generics
The mixin pattern combined with generics can create highly reusable components. Generic constraints ensure that mixin classes have the required structure.
type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
timestamp = Date.now();
};
}
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
const TimestampedUser = Timestamped(User);
const user = new TimestampedUser("Alice");
console.log(user.timestamp); // Current timestamp
Type Inference and Generic Inheritance
TypeScript's type inference capabilities in generic inheritance scenarios are powerful, automatically deducing complex type relationships.
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
map<U>(f: (x: T) => U): Box<U> {
return new Box(f(this.value));
}
}
class ExtendedBox<T> extends Box<T> {
log(): void {
console.log(this.value);
}
}
const box = new ExtendedBox(42);
box.log(); // 42
const stringBox = box.map(x => x.toString());
stringBox.log(); // "42"
Generic Parameter Default Values
Similar to function parameter defaults, generic type parameters can also have default types.
interface PaginatedResponse<T = any> {
data: T[];
total: number;
page: number;
}
const userResponse: PaginatedResponse<{name: string}> = {
data: [{name: "Alice"}],
total: 1,
page: 1
};
const anyResponse: PaginatedResponse = {
data: [1, 2, 3],
total: 3,
page: 1
};
Generics and Decorators
Decorators can be applied to generic classes, but care must be taken with how type information is handled.
function logClass<T extends {new(...args: any[]): {}}>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
console.log(`Instance created: ${constructor.name}`);
}
};
}
@logClass
class GenericEntity<T> {
constructor(public value: T) {}
}
const entity = new GenericEntity<string>("test"); // Output: Instance created: GenericEntity
Generics and Index Types
Combining index types with generics allows for flexible type manipulation tools.
function pluck<T, K extends keyof T>(objs: T[], key: K): T[K][] {
return objs.map(obj => obj[key]);
}
const people = [
{name: "Alice", age: 30},
{name: "Bob", age: 25}
];
const names = pluck(people, "name"); // string[]
const ages = pluck(people, "age"); // number[]
Generics and Recursive Types
Generics can be used to define recursive type structures, which are particularly useful for handling tree-like data.
type TreeNode<T> = {
value: T;
left?: TreeNode<T>;
right?: TreeNode<T>;
};
const numberTree: TreeNode<number> = {
value: 1,
left: {
value: 2,
left: {value: 4}
},
right: {
value: 3
}
};
function traverse<T>(node: TreeNode<T>, visit: (value: T) => void) {
visit(node.value);
if (node.left) traverse(node.left, visit);
if (node.right) traverse(node.right, visit);
}
traverse(numberTree, console.log); // Outputs 1, 2, 4, 3 in order
Generics and Promises
Promises are inherently generic and can represent the eventual result type of asynchronous operations.
function fetchData<T>(url: string): Promise<T> {
return fetch(url).then(response => response.json());
}
interface User {
id: number;
name: string;
}
fetchData<User[]>("/api/users")
.then(users => {
users.forEach(user => console.log(user.name));
});
Generics and React Components
When using TypeScript with React, generics can be used to create reusable components.
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({items, renderItem}: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
}
const users = [{id: 1, name: "Alice"}, {id: 2, name: "Bob"}];
<UserList users={users} />;
function UserList({users}: {users: Array<{id: number, name: string}>}) {
return (
<List
items={users}
renderItem={user => <span>{user.name}</span>}
/>
);
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:泛型与函数重载
下一篇:弱网环境下的体验优化