Расширенные дженерики TypeScript с примерами

Generics в TypeScript предоставляют способ создания повторно используемых и гибких компонентов кода, работая с различными типами данных. Расширенные generics развивают эту концепцию дальше, вводя дополнительные функции, такие как ограничения, значения по умолчанию и множественные типы, которые позволяют разработчикам писать более надежный и типобезопасный код. В этой статье будут использоваться примеры для изучения этих расширенных концепций generics.

Общие ограничения

Ограничения ограничивают типы, которые может принять generic. Это гарантирует, что тип, переданный generic-функции или классу, соответствует определенным критериям. Например, ограничение может использоваться для обеспечения того, чтобы generic-тип имел определенное свойство или метод.

function getLength<T extends { length: number }>(arg: T): number {
    return arg.length;
}

const stringLength = getLength("TypeScript");
const arrayLength = getLength([1, 2, 3]);

В этом примере ограничение <T extends { length: number }> гарантирует, что аргумент, переданный в getLength, имеет свойство length.

Несколько дженериков

TypeScript позволяет использовать несколько универсальных типов в одной и той же функции, классе или интерфейсе. Это полезно при работе с парами значений или другими структурами данных, включающими несколько типов.

function pair<T, U>(first: T, second: U): [T, U] {
    return [first, second];
}

const stringNumberPair = pair("TypeScript", 2024);

Эта функция, pair, принимает два различных универсальных типа, T и U, и возвращает кортеж, содержащий оба типа.

Стандартные универсальные типы

Generics в TypeScript также могут иметь типы по умолчанию. Это полезно, когда вы хотите, чтобы generic имел резервный тип, если не указан конкретный тип.

function identity<T = string>(value: T): T {
    return value;
}

const defaultString = identity("Hello");  // T is string
const customNumber = identity<number>(100);  // T is number

В этом примере, если в identity не передан тип, по умолчанию используется string.

Использование дженериков с интерфейсами

Generics можно использовать с интерфейсами для определения сложных структур, где типы не фиксированы. Это добавляет гибкости в управление данными.

interface Container<T> {
    value: T;
}

const stringContainer: Container<string> = { value: "Hello" };
const numberContainer: Container<number> = { value: 42 };

Интерфейс Container предназначен для хранения значения любого типа, что позволяет использовать различные виды контейнеров с определенными типами.

Универсальные классы

Классы в TypeScript также могут быть универсальными. Это особенно полезно при проектировании классов, работающих с различными типами данных, например, классы хранения данных или коллекции.

class DataStore<T> {
    private data: T[] = [];

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

    getAll(): T[] {
        return this.data;
    }
}

const stringStore = new DataStore<string>();
stringStore.add("Hello");
stringStore.add("TypeScript");

const numberStore = new DataStore<number>();
numberStore.add(42);

В этом примере класс DataStore работает с любым типом данных, предоставляя типобезопасный способ хранения и извлечения элементов.

Заключение

Расширенные обобщения в TypeScript — это мощный инструмент для написания гибкого, повторно используемого и типобезопасного кода. Используя ограничения, множественные типы, значения по умолчанию и обобщения в классах и интерфейсах, разработчики могут писать более сложный и надежный код. Понимание и использование этих расширенных концепций обеспечивает большую гибкость и безопасность типов в приложениях.