TypeScript Generics для повседневного кода
Вы пишете функцию, которая возвращает первый элемент массива. Вы хотите, чтобы она работала для чисел, строк и объектов, но также хотите, чтобы тип возвращаемого значения соответствовал типу входных данных. Без generics вам пришлось бы прибегнуть к any и потерять всю типобезопасность. Здесь на помощь приходят TypeScript generics.
Generics позволяют писать код, который работает с несколькими типами, сохраняя при этом конкретную информацию о типе. В этом руководстве мы рассмотрим практические паттерны использования generics в повседневном коде на TypeScript.
Что такое Generics?
Generics — это способ параметризации типов. Точно так же, как функции принимают параметры, generics принимают параметры типов. Они позволяют создавать переиспользуемые компоненты, которые работают с множеством типов, а не с одним.
Рассмотрим эту функцию идентичности:
function identity<T>(arg: T): T {
return arg;
}
Здесь T — это переменная типа. Когда вы вызываете identity(42), TypeScript выводит T как number. Когда вы вызываете identity("hello"), T становится string. Тип возвращаемого значения всегда совпадает с типом входного.
Generic-функции на практике
Давайте рассмотрим распространённый сценарий: функция, которая оборачивает значение в объект.
function wrapInObject<T>(value: T): { value: T } {
return { value };
}
Без generics вам пришлось бы использовать any или unknown, и вызывающий код потерял бы информацию о типе. С generics тип возвращаемого значения точно равен { value: T }.
Другой повседневный пример — функция, возвращающая последний элемент массива:
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
TypeScript выводит T из аргумента-массива, поэтому last([1, 2, 3]) возвращает number | undefined.
Ограничения Generics (Constraints)
Иногда вы хотите ограничить типы, которые можно использовать с вашим generic. Например, вам может понадобиться доступ к свойству, которое существует только у определённых типов. Вы можете использовать ключевое слово extends, чтобы добавить ограничение.
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
Теперь T должен иметь свойство length типа number. Это работает для строк, массивов и любого объекта со свойством length.
Вы также можете использовать ограничения с интерфейсами:
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
Это гарантирует, что элементы, передаваемые в findById, имеют свойство id.
Generic-интерфейсы и псевдонимы типов
Generics не ограничиваются функциями. Вы можете определять generic-интерфейсы и псевдонимы типов для создания гибких структур данных.
interface ApiResponse<T> {
data: T;
status: number;
error?: string;
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
const data = await response.json();
return { data, status: response.status };
}
Теперь вы можете точно типизировать ответы API:
interface User {
id: number;
name: string;
}
const userResponse = await fetchData<User>('/api/user');
// userResponse.data is User
Generic-классы
Классы также могут быть generic. Распространённый пример — простой стек:
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];
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
const num = numberStack.pop(); // number | undefined
Generics в React-компонентах
Если вы создаёте React-компоненты, generics могут сделать их более переиспользуемыми. Например, компонент списка, который рендерит элементы любого типа:
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>;
}
Теперь вы можете использовать List с любым типом и получить полную проверку типов для колбэка renderItem.
Распространённые ошибки и лучшие практики
- Не злоупотребляйте generics. Если функция работает только с определённым типом, просто используйте этот тип. Generics добавляют сложность.
- Используйте описательные имена параметров типов. Хотя
T— это распространённое имя, для нескольких параметров используйте имена вродеTInput,TOutput. - Предпочитайте ограничения вместо
any. Ограничения дают вам типобезопасность, оставаясь при этом гибкими. - Помните, что generics стираются во время выполнения. Они существуют только на этапе компиляции.
Сравнение: Generics vs. any vs. Union-типы
| Подход | Типобезопасность | Гибкость | Случай использования |
|---|---|---|---|
any |
Отсутствует | Высокая | Миграция JS, быстрые прототипы |
| Union-типы | Хорошая | Ограничена известными типами | Когда вы знаете все возможные типы |
| Generics | Сильная | Высокая | Переиспользуемые компоненты с сохранением типов |
FAQ
Когда следует использовать generics вместо any?
Используйте generics всякий раз, когда хотите сохранить информацию о типе в функции или компоненте. any отключает проверку типов, а generics сохраняют её. Если вы тянетесь к any, чтобы что-то заработало, подумайте, не сможет ли generic решить это более безопасно.
Можно ли использовать несколько параметров типов?
Да. Вы можете определять функции с несколькими параметрами типов, например function pair<K, V>(key: K, value: V): [K, V]. Используйте описательные имена, когда их больше одного.
Как generics работают со стрелочными функциями в файлах .tsx?
В файлах .tsx <T> может быть неоднозначным с JSX. Используйте завершающую запятую: const identity = <T,>(arg: T): T => arg; или используйте extends unknown: <T extends unknown>.
Generics — это мощная функция, которая может сделать ваш код на TypeScript более переиспользуемым и типобезопасным. Начните с простых generic-функций, затем постепенно применяйте их к интерфейсам и классам. Если вам нужно форматировать или валидировать JSON-данные с типобезопасностью, ознакомьтесь с нашим инструментом JSON Formatter, чтобы быстро просматривать и форматировать ваш JSON.