阿里云主机折上折
  • 微信号
Current Site:Index > Generics and Inheritance

Generics and Inheritance

Author:Chuan Chen 阅读数:42682人阅读 分类: TypeScript

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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.