$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогGenerics в TypeScript: пишем универсальный код без потери типобезопасности

Generics в TypeScript: пишем универсальный код без потери типобезопасности

$ sudo teach IT
·18 июля 2026 г.·8 мин·71
Generics в TypeScript: пишем универсальный код без потери типобезопасности

Разбираем обобщённые типы на практике — от простых функций до сложных ограничений с extends и keyof. После статьи вы перестанете избегать дженериков и начнёте использовать их осознанно.

Содержание
Зачем вообще нужны дженерикиСинтаксис и соглашенияНесколько параметров типаОграничения через extendsОграничение через конкретный типkeyof и работа со свойствами объектовПрактический пример: безопасное обновление объектаУсловные типы и дженерикиДженерики в React: реальный сценарийТипичные ошибки и подводные камниГде учиться дальшеИтог

Если вы работаете с TypeScript больше нескольких недель, вы наверняка уже видели угловые скобки в чужом коде — Array<string>, Promise<void>, Record<string, number>. Возможно, даже писали что-то подобное сами, не до конца понимая механику. А может, намеренно обходили дженерики стороной, заменяя их на any или дублируя похожие функции под разные типы.

Это нормальная история. Generics — одна из тех концепций, которые выглядят пугающе абстрактно до тех пор, пока не щёлкнет. После этого вы начинаете видеть их везде и удивляться, как вообще обходились без них.

В этой статье разберём дженерики системно: от базового синтаксиса до ограничений через extends и keyof, с реальными примерами кода и объяснением того, почему именно так, а не иначе.


Зачем вообще нужны дженерики

Начнём с проблемы, которую они решают. Представьте, что вам нужна функция, которая возвращает первый элемент массива:

code
function first(arr: number[]): number {
  return arr[0];
}

Работает. Но что если массив строк? Придётся написать ещё одну:

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

Это очевидно плохо — логика одинаковая, а кода вдвое больше. Можно попробовать уйти в any:

code
function first(arr: any[]): any {
  return arr[0];
}

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

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

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

const num = first([1, 2, 3]);       // num: number
const str = first(['a', 'b', 'c']); // str: string

T здесь — это параметр типа. Как обычный параметр функции, только для типов. TypeScript сам выводит T из аргумента, который вы передаёте — это называется type inference, и именно благодаря ему вам не нужно каждый раз писать first<number>([1, 2, 3]) вручную.


Синтаксис и соглашения

Параметр типа объявляется в угловых скобках перед списком аргументов функции, после имени класса или интерфейса:

code
// Функция
function identity<T>(value: T): T {
  return value;
}

// Стрелочная функция (в .tsx-файлах нужна запятая или extends)
const identity = <T,>(value: T): T => value;

// Интерфейс
interface Box<T> {
  value: T;
  label: string;
}

// Класс
class Stack<T> {
  private items: T[] = [];

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

  pop(): T | undefined {
    return this.items.pop();
  }
}

По соглашению параметры типов именуют одной заглавной буквой: T (Type), K (Key), V (Value), E (Element). Это не обязательно — можно писать TItem или InputType — но однобуквенные имена стали стандартом де-факто в большинстве кодовых баз.


Несколько параметров типа

Дженерики могут принимать несколько параметров. Классический пример — функция, которая создаёт пару значений:

code
function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second];
}

const result = pair('hello', 42); // [string, number]

Или более практичный случай — функция трансформации объекта:

code
function mapObject<K extends string, V, R>(
  obj: Record<K, V>,
  transform: (value: V) => R
): Record<K, R> {
  const result = {} as Record<K, R>;
  for (const key in obj) {
    result[key] = transform(obj[key]);
  }
  return result;
}

const prices = { apple: '1.5', banana: '0.8' };
const parsed = mapObject(prices, parseFloat);
// parsed: Record<"apple" | "banana", number>

Здесь уже появляется extends — давайте разберём его подробно.


Ограничения через extends

По умолчанию параметр типа T может быть чем угодно. Иногда это слишком широко — нам нужно гарантировать, что тип обладает определёнными свойствами.

Допустим, мы хотим написать функцию, которая возвращает длину чего-либо:

code
function getLength<T>(item: T): number {
  return item.length; // Ошибка: Property 'length' does not exist on type 'T'
}

TypeScript справедливо ругается: он не знает, что у T есть свойство length. Исправляем через ограничение:

code
interface HasLength {
  length: number;
}

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

getLength('hello');        // 5
getLength([1, 2, 3]);      // 3
getLength({ length: 10 }); // 10
getLength(42);             // Ошибка: number не имеет length

T extends HasLength означает: «T может быть любым типом, но только если он совместим с HasLength». Это структурная типизация в действии — TypeScript не требует явного наследования, достаточно наличия нужных полей.

Ограничение через конкретный тип

Ограничение не обязано быть интерфейсом. Можно ограничить тип конкретным значением или объединением:

code
type Direction = 'left' | 'right' | 'up' | 'down';

function move<T extends Direction>(direction: T, steps: number): string {
  return `Moving ${direction} by ${steps} steps`;
}

move('left', 5);   // OK
move('diagonal');  // Ошибка

Или ограничить тип так, чтобы он был классом:

code
function createInstance<T>(constructor: new () => T): T {
  return new constructor();
}

Синтаксис new () => T описывает конструктор без аргументов, возвращающий T. Это полезно при работе с фабриками и dependency injection.


keyof и работа со свойствами объектов

Оператор keyof возвращает объединение всех ключей типа. В сочетании с дженериками это открывает мощные возможности для типобезопасной работы с объектами.

Классический пример — функция getProperty:

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

const user = { name: 'Alice', age: 30, active: true };

const name = getProperty(user, 'name');   // string
const age = getProperty(user, 'age');     // number
getProperty(user, 'email');               // Ошибка: не существует в типе

Разберём по частям:

  • T — тип объекта
  • K extends keyof T — K должен быть одним из ключей T
  • T[K] — тип значения по ключу K (indexed access type)

TypeScript не просто проверяет, что ключ существует — он выводит точный тип возвращаемого значения. Это называется lookup type или indexed access type.

Практический пример: безопасное обновление объекта

code
function updateField<T, K extends keyof T>(
  obj: T,
  key: K,
  value: T[K]
): T {
  return { ...obj, [key]: value };
}

const user = { name: 'Alice', age: 30 };
const updated = updateField(user, 'age', 31);    // OK
updateField(user, 'age', 'thirty-one');           // Ошибка: string не совместим с number
updateField(user, 'email', 'alice@example.com'); // Ошибка: ключ не существует

Это намного безопаснее, чем (obj as any)[key] = value, и при этом не требует перегрузок.


Условные типы и дженерики

TypeScript позволяет писать условные типы — это следующий уровень работы с дженериками. Синтаксис напоминает тернарный оператор:

code
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

Более практичный пример — извлечение типа из Promise:

code
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>;  // string
type B = UnwrapPromise<number>;           // number

Ключевое слово infer позволяет «захватить» часть типа внутри условного выражения. Это используется в стандартной библиотеке TypeScript — например, ReturnType<T> реализован именно так:

code
type ReturnType<T extends (...args: any) => any> = 
  T extends (...args: any) => infer R ? R : any;

Дженерики в React: реальный сценарий

Если вы пишете на React с TypeScript, дженерики встречаются постоянно. Типичный пример — обобщённый компонент списка:

code
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Использование
<List
  items={users}
  keyExtractor={user => user.id}
  renderItem={user => <span>{user.name}</span>}
/>

Компонент не знает ничего о конкретном типе данных, но TypeScript выведет T из items и проверит, что renderItem и keyExtractor работают с тем же типом.


Типичные ошибки и подводные камни

1. Лишние дженерики там, где они не нужны

Не каждая функция должна быть обобщённой. Если тип всегда одинаковый — просто укажите его явно. Дженерики оправданы, когда тип действительно варьируется.

2. Использование any внутри обобщённой функции

code
// Плохо: теряем информацию о типе
function wrap<T>(value: T): { data: any } {
  return { data: value };
}

// Хорошо
function wrap<T>(value: T): { data: T } {
  return { data: value };
}

3. Слишком широкие ограничения

T extends object — очень широко. Если вам нужен объект с конкретными полями, опишите интерфейс.

4. Путаница между T extends U и T implements U

В TypeScript нет implements для типов — только extends. И это структурная проверка, не номинальная.

5. Проблемы с вызовом дженериков в .tsx

В JSX-файлах <T> может быть воспринят как JSX-тег. Решение — добавить запятую или ограничение:

code
const identity = <T,>(value: T): T => value;
// или
const identity = <T extends unknown>(value: T): T => value;

Где учиться дальше

Дженерики — это не отдельная фича, а фундаментальная часть системы типов TypeScript. Чем глубже вы погружаетесь в язык, тем чаще они встречаются: в утилитарных типах (Partial, Required, Pick, Omit), в типизации API-клиентов, в паттернах вроде Builder или Repository.

Если вы только начинаете разбираться с TypeScript и хотите выстроить понимание с нуля — в том числе освоить дженерики в контексте реальных задач — стоит посмотреть на структурированный курс. Например, курс «TypeScript — для начинающих!» охватывает именно такой путь: от базового синтаксиса до практических паттернов, которые встречаются в реальных проектах.


Итог

Generics — это не магия и не синтаксический сахар. Это инструмент, который позволяет писать код один раз и при этом сохранять полную информацию о типах. Ключевые идеи, которые стоит унести из этой статьи:

  • Параметр типа T — это переменная для типа, которую TypeScript выводит из контекста
  • extends ограничивает допустимые типы, не уничтожая обобщённость
  • keyof в связке с дженериками даёт типобезопасный доступ к свойствам объектов
  • Условные типы и infer открывают возможности для сложных трансформаций типов
  • Дженерики — не цель, а средство: используйте их там, где они реально упрощают код

Если после прочтения вы открыли свой проект и нашли несколько мест, где any можно заменить на обобщённый тип — статья сделала своё дело. А если хотите разобраться глубже и систематически — курс «TypeScript — для начинающих!» может стать хорошей отправной точкой для этого пути.

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

TypeScript - для начинающих!

Курс по теме

TypeScript - для начинающих!

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

Бесплатно
Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Приложения для iPhone и Apple Watch на SwiftUI

Приложения для iPhone и Apple Watch на SwiftUI

Разработка приложений для iPhone и Apple Watch на SwiftUI: навигация, SwiftData, виджеты, часы, выпуск. Нужен Mac с Xcode 27, сами устройства не нужны.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Приложения для macOS на SwiftUI

Приложения для macOS на SwiftUI

Разработка приложений для Mac на SwiftUI: окна и меню, Liquid Glass, SwiftData, сеть, выпуск. Нужен Mac с macOS 27 и Xcode 27.

Перейти к курсу

Другие статьи

Модели данных и типизация в TypeScript: как проектировать контракт между фронтом и API
модели

Модели данных и типизация в TypeScript: как проектировать контракт между фронтом и API

Разберём, как описывать доменные сущности, DTO и error-model на уровне типов, чтобы ловить несовпадения схем до продакшена. Покажем практические шаблоны для типобезопасного маппинга и версионирования контрактов.

15 сентября 2026 г.
1040
Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта
пишем

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта

Построим удобный интерфейс командной утилиты: структура подкоманд, чтение конфигов, валидация входных параметров и единая логика ошибок с понятными exit code. Сфокусируемся на UX для тех, кто запускает CLI в CI и в скриптах.

15 сентября 2026 г.
1010
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1421
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1070
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Зачем вообще нужны дженерикиСинтаксис и соглашенияНесколько параметров типаОграничения через extendsОграничение через конкретный типkeyof и работа со свойствами объектовПрактический пример: безопасное обновление объектаУсловные типы и дженерикиДженерики в React: реальный сценарийТипичные ошибки и подводные камниГде учиться дальшеИтог