TypeScript Generics для повседневного кода: практическое руководство
Вы пишете функцию, которая работает с любым типом, но TypeScript продолжает жаловаться на any. Или вы скопировали одну и ту же функцию три раза, чтобы обработать разные типы. Generics решают эту проблему — они позволяют писать код, который работает с несколькими типами, сохраняя типобезопасность. Но многие разработчики либо избегают их, либо злоупотребляют ими. Это руководство покажет, как использовать generics в повседневном коде без головной боли.
Что такое Generics и почему это важно?
Generics — это способ параметризации типов. Вместо того чтобы жестко задавать тип, например string или number, вы используете переменную типа (часто T), которая заменяется конкретным типом при использовании функции или класса.
Почему это важно? Потому что это позволяет писать переиспользуемый код, остающийся типобезопасным. Без generics вам пришлось бы либо дублировать код, либо использовать any, что сводит на нет смысл TypeScript.
Generic Functions: строительный блок
Начнем с простого примера. Предположим, вы хотите функцию, возвращающую первый элемент массива.
function firstElement(arr: any[]): any {
return arr[0];
}
Это работает, но вы теряете информацию о типе. Если вы передадите массив строк, возвращаемый тип будет any, а не string. Теперь с generics:
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
Теперь TypeScript знает, что если вы передадите string[], вы получите обратно string | undefined. Тип сохраняется.
Вы также можете использовать несколько параметров типа:
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
Generic Interfaces и Type Aliases
Generics не ограничиваются функциями. Вы можете создавать generic интерфейсы и псевдонимы типов для описания гибких структур данных.
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
Это особенно полезно для ответов API. Вместо определения нового интерфейса для каждой конечной точки вы можете определить generic ApiResponse<T>:
interface ApiResponse<T> {
data: T;
status: number;
}
type User = { id: number; name: string };
const response: ApiResponse<User> = {
data: { id: 1, name: "Alice" },
status: 200,
};
Generic Constraints: добавление правил
Иногда вы хотите ограничить типы, которые можно использовать с вашим generic. Здесь на помощь приходят constraints. Используйте ключевое слово extends, чтобы требовать наличия определенных свойств у типа.
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello"); // OK
getLength([1, 2, 3]); // OK
getLength(42); // Error: number doesn't have length
Constraints делают ваши generics более предсказуемыми и предотвращают ошибки времени выполнения.
Generic Classes
Классы также могут быть generic. Это удобно для таких структур данных, как стеки, очереди или репозитории.
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const top = numberStack.pop(); // number | undefined
Когда использовать Generics (а когда нет)
Generics мощны, но при чрезмерном использовании они могут усложнить чтение кода. Вот краткое руководство:
| Используйте Generics, когда | Избегайте Generics, когда |
|---|---|
| Вам нужно сохранить связи между типами | Достаточно простого union-типа или any |
| Вы пишете переиспользуемые утилиты | Тип всегда один и тот же |
| Вы хотите избежать дублирования кода | Это затрудняет понимание кода |
Хорошее эмпирическое правило: если вы пишете одну и ту же логику для нескольких типов, рассмотрите generics. Если вы добавляете третий параметр типа, и никто его не понимает, сделайте шаг назад.
Распространенные паттерны и лучшие практики
- Используйте описательные имена для параметров типа.
Tподходит для простых случаев, ноTDataилиTResponseмогут быть понятнее. - Предпочитайте constraints вместо
any. Если нужна гибкость, используйтеextendsдля принудительного соблюдения минимальной структуры. - Используйте параметры типа по умолчанию. Вы можете указать значение по умолчанию:
<T = string>. - Используйте generics с utility types. Встроенные в TypeScript
Partial<T>,Readonly<T>иRecord<K, V>являются generic.
Пример из реальной жизни: Generic API Client
Давайте свяжем все вместе на практическом примере: простой API-клиент, который получает данные и возвращает типизированный ответ.
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
async function main() {
const post = await fetchData<Post>("https://jsonplaceholder.typicode.com/posts/1");
console.log(post.title); // string
}
Этот паттерн часто встречается во фронтенд-коде. Вы получаете типобезопасность без повторений для каждой конечной точки.
FAQ
Что означает синтаксис <T> в TypeScript?
<T> объявляет параметр типа. Это заполнитель для типа, который будет указан при использовании функции, интерфейса или класса. Вы можете назвать его как угодно, но T — традиционное имя для одного типа.
Могу ли я использовать несколько generic параметров типа?
Да. Вы можете определить столько, сколько нужно, разделяя их запятыми: <T, U, V>. Используйте описательные имена, чтобы код оставался читаемым.
Как ограничить generic, чтобы он принимал только объекты?
Используйте T extends object. Это гарантирует, что T является объектным типом, а не примитивом, таким как string или number. Вы также можете ограничить конкретными структурами, например T extends { id: number }.
Generics — это основа написания поддерживаемого TypeScript. Если вы работаете с данными JSON и хотите быстро проверить или отформатировать их, попробуйте наш JSON Formatter для удобного форматирования и просмотра ответов API.