Generics в TypeScript: первый пример без головоломок

Generics в TypeScript часто объясняют так, что новичок успевает испугаться раньше, чем увидит пользу. Угловые скобки, буква T, какие-то ограничения, keyof, extends. Кажется, будто это тема для людей, которые пишут библиотеки, а не обычный код

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

Если сказать еще проще: мы хотим переиспользовать код и не скатываться в any

Проблема без generics

Допустим, нужна функция, которая возвращает первый элемент массива:

function firstString(items: string[]): string {
  return items[0];
}

function firstNumber(items: number[]): number {
  return items[0];
}

Код почти одинаковый. Отличается только тип

Можно написать через any:

function firstAny(items: any[]): any {
  return items[0];
}

Но мы потеряли пользу TypeScript. Если передать массив строк, TypeScript уже не помнит, что результат строка

const title = firstAny(["TypeScript", "JavaScript"]);
title.toFixed(2);

TypeScript не остановит, потому что title стал any. А у строки нет toFixed

Первый generic

Напишем универсальную функцию:

function first<T>(items: T[]): T {
  return items[0];
}

T — это параметр типа. Можно читать так: "функция работает с массивом каких-то элементов T и возвращает один элемент T"

Примеры:

const title = first(["TypeScript", "JavaScript"]);
const score = first([10, 20, 30]);

TypeScript понимает:

  • title — string;
  • score — number.

Мы не писали две функции и не использовали any

Почему T, а не другое имя

T — просто традиционное имя. Можно написать:

function first<Item>(items: Item[]): Item {
  return items[0];
}

Так иногда даже понятнее. В маленьких примерах часто используют T, в более сложных типах лучше давать говорящие имена: Item, ResponseData, Entity

Главное — не думать, что T волшебная буква. Это переменная для типа

Generic type для ответа API

Очень практический пример — ответ API. Допустим, сервер всегда возвращает такую обертку:

{
  "data": "...",
  "success": true,
  "error": null
}

Но data бывает разной: пользователь, список уроков, заказ

Можно описать generic:

type ApiResponse<Data> = {
  data: Data;
  success: boolean;
  error: string | null;
};

Теперь пользователь:

type User = {
  id: number;
  name: string;
};

const userResponse: ApiResponse<User> = {
  data: {
    id: 1,
    name: "Алина",
  },
  success: true,
  error: null,
};

А список уроков:

type Lesson = {
  title: string;
  minutes: number;
};

const lessonsResponse: ApiResponse<Lesson[]> = {
  data: [
    { title: "Generics", minutes: 30 },
  ],
  success: true,
  error: null,
};

Одна обертка, разные данные, типы не потерялись

Generic с ограничением extends

Иногда generic слишком свободный. Например, функция должна работать с объектами, у которых точно есть id

Плохой вариант:

function printId<T>(item: T): string {
  return `ID: ${item.id}`;
}

TypeScript ругнется: он не знает, что у любого T есть id

Добавим ограничение:

function printId<T extends { id: number | string }>(item: T): string {
  return `ID: ${item.id}`;
}

Теперь функция принимает не что угодно, а только значения с id

printId({ id: 10, title: "TypeScript" });
printId({ name: "No id" });

Второй вызов подсветится. Это и есть смысл extends: тип должен подходить под минимальную форму

keyof: взять ключ объекта

Еще один полезный пример:

function getValue<T, Key extends keyof T>(object: T, key: Key): T[Key] {
  return object[key];
}

На первый взгляд плотновато, разберем:

  • T — тип объекта;
  • Key extends keyof T — ключ должен быть одним из ключей объекта;
  • T[Key] — тип значения по этому ключу.

Пример:

const lesson = {
  title: "Generics",
  minutes: 30,
};

const title = getValue(lesson, "title");
const minutes = getValue(lesson, "minutes");
const missing = getValue(lesson, "author");

"author" подсветится, потому что такого ключа нет

Это уже не первый шаг, но хороший пример пользы generics: TypeScript связывает ключ и результат

Когда generic не нужен

Не надо писать generic ради красоты:

function double<T>(value: T): T {
  return value * 2;
}

Такой код неправильный по смыслу. Умножать можно не любой T

Лучше честно:

function double(value: number): number {
  return value * 2;
}

Generic нужен, когда тип действительно должен оставаться гибким: массив любых элементов, API-обертка, переиспользуемый компонент, функция выбора значения из объекта

Частые ошибки

Первая ошибка — заменять generics на any. Код вроде проще, но TypeScript перестает помнить тип результата

Вторая ошибка — добавлять generic там, где достаточно обычного number или string

Третья ошибка — использовать одну букву в сложном типе, где лучше дать имя. ResponseData иногда читается лучше, чем T

Четвертая ошибка — забывать ограничения extends. Если функция работает только с объектами с id, это нужно сказать TypeScript

Мини-задание

Напиши generic-тип:

type ListResult<Item> = {
  items: Item[];
  total: number;
};

Потом опиши урок:

type Lesson = {
  id: number;
  title: string;
};

И создай результат:

const result: ListResult<Lesson> = {
  items: [
    { id: 1, title: "Generics" },
  ],
  total: 1,
};

После этого специально убери title из объекта и посмотри, как TypeScript подсветит ошибку

Что может быть еще интересно по этой теме

Generics нужны только для библиотек?

Нет. Они часто встречаются в обычном коде: API responses, React components, helper functions, collections

Почему не использовать any?

any стирает информацию о типе. Generic позволяет переиспользовать код и сохранить конкретный тип результата

Можно ли назвать T по-другому?

Да. T — традиция, а не правило. В прикладном коде часто лучше писать Item, Data, Entity

Когда стоит остановиться?

Если тип стал сложнее задачи и никто не понимает ошибку компилятора, возможно, generic слишком рано усложнил код. Начинай с простого типа и обобщай только при повторе

Что почитать дальше по TypeScript

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x