Урок 06

Дженерик-классы

Классы могут принимать параметр типа. Полезно для коллекций, стеков, хранилищ.

Базовый пример

class Stack<T> {
    private items: T[] = [];

    push(item: T): void {
        this.items.push(item);
    }

    pop(): T | undefined {
        return this.items.pop();
    }

    peek(): T | undefined {
        return this.items[this.items.length - 1];
    }

    get size(): number {
        return this.items.length;
    }
}

const numbers = new Stack<number>();
numbers.push(1);
numbers.push(2);
numbers.push('a'); // ошибка

С ограничением

class Repository<T extends { id: number }> {
    private items: T[] = [];

    add(item: T) {
        this.items.push(item);
    }

    find(id: number): T | undefined {
        return this.items.find((item) => item.id === id);
    }
}

const users = new Repository<{ id: number; name: string }>();
users.add({ id: 1, name: 'Алиса' });

Несколько параметров

class Pair<K, V> {
    constructor(public key: K, public value: V) {}

    swap(): Pair<V, K> {
        return new Pair(this.value, this.key);
    }
}

const p = new Pair('age', 25);
const swapped = p.swap(); // Pair<number, string>

Методы с дженериками

class Collection<T> {
    private items: T[] = [];

    add(item: T) {
        this.items.push(item);
    }

    map<U>(fn: (item: T) => U): U[] {
        return this.items.map(fn);
    }
}

const col = new Collection<number>();
col.add(1);
col.add(2);
const strings = col.map((n) => n.toString()); // string[]

Метод может добавить свой параметр U, независимый от T класса.

Наследование

class Base<T> {
    constructor(public value: T) {}
}

class Wrapper<T> extends Base<T> {
    wrap(): T[] {
        return [this.value];
    }
}

const w = new Wrapper('hello');
w.value; // string

Наследование с фиксированным типом

class StringWrapper extends Base<string> {
    toUpperCase(): string {
        return this.value.toUpperCase();
    }
}

Абстрактный класс

abstract class Storage<T> {
    abstract get(key: string): T | undefined;
    abstract set(key: string, value: T): void;
}

class MemoryStorage<T> extends Storage<T> {
    private data = new Map<string, T>();

    get(key: string): T | undefined {
        return this.data.get(key);
    }

    set(key: string, value: T): void {
        this.data.set(key, value);
    }
}

Статические методы

Статические методы не могут использовать T класса:

class Container<T> {
    // static create(): Container<T> {} // ошибка

    static create<U>(value: U): Container<U> {
        return new Container<U>(value);
    }

    constructor(public value: T) {}
}

const c = Container.create('hello'); // Container<string>

Практика: EventEmitter

type Events = Record<string, unknown>;

class EventEmitter<T extends Events> {
    private listeners: { [K in keyof T]?: ((payload: T[K]) => void)[] } = {};

    on<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
        this.listeners[event] = this.listeners[event] ?? [];
        this.listeners[event]!.push(listener);
    }

    emit<K extends keyof T>(event: K, payload: T[K]): void {
        this.listeners[event]?.forEach((l) => l(payload));
    }
}

type AppEvents = {
    login: { userId: number };
    logout: { userId: number };
};

const bus = new EventEmitter<AppEvents>();
bus.on('login', ({ userId }) => console.log(userId));
bus.emit('login', { userId: 1 });

Итоги

  • Классы принимают параметр типа
  • Методы могут добавлять свои параметры
  • Наследование сохраняет дженерик
  • Абстрактные классы работают
  • Статические методы — свои параметры