Перейти к содержанию
Educora
Продвинутый18 мин7 / 8

Дженерики и служебные типы

Пиши функции и интерфейсы, которые работают с любым типом и при этом не теряют информацию о типах, ограничивай их через `extends` и `keyof` и создавай новые типы с помощью `Partial`, `Pick`, `Omit`, `Record` и `ReturnType`.

Проверь себя
В этом уроке ты узнаешь
  • Писать обобщённую функцию с параметром типа <T>
  • Описывать обобщённые интерфейсы и типы
  • Ограничивать параметры типа через extends и keyof
  • Применять служебные типы Partial, Pick, Omit, Record и ReturnType

Нужна функция, которая возвращает первый элемент массива: для городов, для цен, для пользователей. Писать отдельные firstString, firstNumber и firstUser бессмысленно. А any уничтожает информацию о типе. Код ниже компилируется без ошибок, но при запуске падает с TypeError: city.toFixed is not a function — компилятор уже не знает, что city — строка:

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

const city = firstAny(['Baku', 'Ganja']);
console.log(city.toFixed(2));

Обобщённые функции

Определение
Дженерик (обобщённый тип)

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

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

const city = first(['Baku', 'Ganja']);
const score = first([90, 75]);
const empty = first<boolean>([]);

console.log(city?.toUpperCase(), score?.toFixed(1), empty);
Ожидаемый результат
BAKU 90.0 undefined
city имеет тип string | undefined, а score — number | undefined. Массив может быть пустым, поэтому учтён и undefined.

Здесь T означает «тип, который пока неизвестен». В вызове first(['Baku', 'Ganja']) TypeScript смотрит на аргумент и сам выводит, что T — это string. Тип можно указать и явно: first<boolean>([]). Это нужно, когда TypeScript не может определить тип сам, как в случае с пустым массивом.

Обобщённые интерфейсы

Параметр может принимать и сам тип. Сервер всегда отвечает в одинаковой форме — статус и данные, — но данные в каждом запросе разные. Вместо отдельного интерфейса на каждый случай пишем один обобщённый. На самом деле ты уже пользовался дженериками: Array<string> и Promise<number> — тоже обобщённые типы.

TypeScript
interface ApiResponse<T> {
  status: number;
  data: T;
}

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

const userResponse: ApiResponse<User> = {
  status: 200,
  data: { id: 1, name: 'Leyla' },
};

const tagsResponse: ApiResponse<string[]> = {
  status: 200,
  data: ['typescript', 'generics'],
};

console.log(userResponse.data.name, tagsResponse.data.length);
Ожидаемый результат
Leyla 2

Ограничения: extends и keyof

Иногда T не может быть «каким угодно типом». Функции, возвращающей более длинное из двух значений, нужно свойство length. Запись T extends { length: number } — это ограничение: T может быть только типом со свойством length — строкой, массивом и т. п. А у чисел length нет:

TypeScript
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

console.log(longest('Baku', 'Sumgait'));
console.log(longest([1, 2, 3], [4, 5]));
longest(10, 20);
Text
longest.ts:7:9 - error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

7 longest(10, 20);
          ~~
Если удалить ошибочную строку, программа выведет Sumgait и [ 1, 2, 3 ].

Оператор keyof T даёт объединение всех ключей объектного типа: для { title: string; year: number } это 'title' | 'year'. А T[K] — тип значения по этому ключу. Вместе они дают функцию, которая принимает только существующие свойства и возвращает точно нужный тип:

TypeScript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const book = { title: 'Ali and Nino', year: 1937 };

const title = getProperty(book, 'title');
const year = getProperty(book, 'year');
console.log(title.toUpperCase(), year + 1);
getProperty(book, 'author');
Text
book.ts:10:19 - error TS2345: Argument of type '"author"' is not assignable to parameter of type '"title" | "year"'.

10 getProperty(book, 'author');
                     ~~~~~~~~
title получает тип string, а year — number. Несуществующий ключ 'author' отклоняется.

Служебные типы

В TypeScript есть готовые служебные типы (utility types), построенные на дженериках. Они создают новый тип из существующего, и одни и те же свойства не приходится писать дважды. Чаще всего используются эти:

ТипЧто делает
Partial<T>делает все свойства необязательными
Required<T>делает все свойства обязательными
Readonly<T>делает все свойства доступными только для чтения
Pick<T, K>оставляет только выбранные свойства K
Omit<T, K>оставляет всё, кроме свойств K
Record<K, V>объект с ключами типа K и значениями типа V
ReturnType<F>тип, который возвращает функция F
TypeScript
interface Product {
  id: number;
  name: string;
  price: number;
  stock: number;
}

const laptop: Product = { id: 1, name: 'Laptop', price: 1500, stock: 4 };

function update(product: Product, changes: Partial<Product>): Product {
  return { ...product, ...changes };
}

type ProductCard = Pick<Product, 'name' | 'price'>;
type NewProduct = Omit<Product, 'id'>;

const card: ProductCard = { name: laptop.name, price: laptop.price };
const draft: NewProduct = { name: 'Mouse', price: 25, stock: 30 };

console.log(update(laptop, { price: 1350 }));
console.log(card, draft.name);
Ожидаемый результат
{ id: 1, name: 'Laptop', price: 1350, stock: 4 }
{ name: 'Laptop', price: 1500 } Mouse
Partial — для обновления (только изменённые свойства), Omit<Product, 'id'> — для нового товара (id назначает база данных), Pick — для маленькой карточки.
TypeScript
type City = 'Baku' | 'Ganja' | 'Shaki';

const deliveryDays: Record<City, number> = {
  Baku: 1,
  Ganja: 2,
  Shaki: 3,
};

function createUser(name: string, age: number) {
  return { name, age, createdAt: '2025-09-01' };
}

type User = ReturnType<typeof createUser>;

const user: User = createUser('Elvin', 16);
console.log(deliveryDays.Ganja, user.name, user.createdAt);
Ожидаемый результат
2 Elvin 2025-09-01
Record<City, number> требует значение для каждого города: забудешь Shaki — получишь ошибку TS2741.

Главное

  • Дженерик принимает тип как параметр: function first<T>(items: T[]): T | undefined.
  • T обычно выводится из аргумента, а при необходимости указывается явно: first<boolean>([]).
  • T extends X ограничивает параметр типа; K extends keyof T разрешает только существующие ключи.
  • Partial, Pick, Omit, Record и ReturnType создают новые типы из существующих.
  • Если параметр типа не связывает хотя бы два места, дженерик не нужен.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Что такое <T> в записи function first<T>(items: T[]): T | undefined?