В разработке масштабируемых приложений ключевую роль играет повторное использование кода без потери типобезопасности. Без обобщенного программирования разработчики вынуждены либо дублировать идентичные структуры для каждого типа данных, либо прибегать к типу any, полностью разрушающему статическую верификацию.
Дженерики (Generics / Обобщения) — это параметры для типов. Точно так же, как обычные функции принимают аргументы-значения для вычисления результата в рантайме, конструкторы типов принимают параметры типа (<T>) для вычисления результирующего статического типа на этапе компиляции.
На базе дженериков стандартная библиотека TypeScript предоставляет семейство встроенных утилитных типов (Utility Types): Partial<T>, Required<T>, Readonly<T>, Pick<T, K>, Omit<T, K>, Record<K, T>, ReturnType<T> и других. Эти утилиты позволяют трансформировать существующие модели данных без ручного дублирования интерфейсов.
Взглянем на соответствие между миром значений и миром типов:
+-------------------------------------------------------------------------+
| АНАЛОГИЯ: ЗНАЧЕНИЯ vs ТИПЫ |
+-------------------------------------------------------------------------+
| МИР ЗНАЧЕНИЙ (Рантайм): |
| function wrapValue(val) { |
| return { data: val }; |
| } |
| wrapValue(42) ---> { data: 42 } |
+-------------------------------------------------------------------------+
| МИР ТИПОВ (Компиляция): |
| type ApiResponse<T> = { |
| data: T; |
| status: number; |
| }; |
| ApiResponse<User> ---> { data: User; status: number } |
| ApiResponse<Post> ---> { data: Post; status: number } |
+-------------------------------------------------------------------------+
| СТАНДАРТНЫЕ ТРАНСФОРМАЦИИ (Utility Types): |
| Partial<User> ---> Все поля становятся опциональными (?) |
| Pick<User, 'id'> ---> Оставляет ТОЛЬКО указанные ключи |
| Omit<User, 'pwd'> ---> Исключает указанные ключи из типа |
+-------------------------------------------------------------------------+<T>)
Разработчик вызывает функцию identity(42) без явного указания identity<number>(42).
Компилятор исследует позицию аргумента и связывает значение 42 с типовой переменной T.
Если объявлено T extends Entity, компилятор сверяет выведенный тип с базовым ограничением.
Тип T фиксируется, и все зависимые позиции (возвращаемый тип, другие аргументы) получают точный тип.
Какую роль выполняет ключевое слово `extends` в объявлении `function getEntityId<T extends { id: string }>(entity: T): string`?
apiClient.get<UserProfile>('/me'), гарантирующие правильный тип данных ответа.const [user, setUser] = useState<User | null>(null).Partial<User> для эндпоинтов PATCH-запросов.Record<string, CacheEntry> или Record<Locale, Dictionary>.Компилятор автоматически выводит тип T на основе переданного аргумента:
// Обобщенная функция-обертка
function wrapResult<T>(value: T): { success: true; payload: T } {
return { success: true, payload: value };
}
const numResult = wrapResult(100); // Тип: { success: true; payload: number }
const strResult = wrapResult('OK'); // Тип: { success: true; payload: string }
// Обобщенный интерфейс контейнера
interface PaginatedList<TItem> {
items: TItem[];
total: number;
page: number;
hasMore: boolean;
}
interface Article {
slug: string;
title: string;
}
type ArticleFeed = PaginatedList<Article>;Partial, Pick, Omit, Record#interface UserProfile {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
}
// 1. Partial<T>: делает все поля опциональными (для PATCH-запросов)
type UpdateUserDto = Partial<UserProfile>;
// Эквивалентно: { id?: string; name?: string; email?: string; role?: 'admin' | 'user'; createdAt?: Date }
// 2. Pick<T, K>: выбирает только указанные поля
type UserPreview = Pick<UserProfile, 'id' | 'name'>;
// Эквивалентно: { id: string; name: string }
// 3. Omit<T, K>: исключает указанные поля (например, убираем id и createdAt при создании)
type CreateUserDto = Omit<UserProfile, 'id' | 'createdAt'>;
// Эквивалентно: { name: string; email: string; role: 'admin' | 'user' }
// 4. Record<K, T>: создает типизированный словарь ключ-значение
type UserRegistry = Record<string, UserProfile>;
type RolePermissions = Record<'admin' | 'user', string[]>;
const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
user: ['read'],
};keyof#Связывание двух параметров типа позволяет создавать абсолютно безопасные хелперы доступа к свойствам:
// K обязан быть ключом объекта T!
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const profile: UserProfile = {
id: 'usr_1',
name: 'Anna',
email: '[email protected]',
role: 'admin',
createdAt: new Date(),
};
const userEmail = getProperty(profile, 'email'); // Тип userEmail строго string!
const creationDate = getProperty(profile, 'createdAt'); // Тип строго Date!
// ОШИБКА КОМПИЛЯЦИИ:
// getProperty(profile, 'nonExistentProperty');
// TS2345: Argument of type '"nonExistentProperty"' is not assignable to parameter of type 'keyof UserProfile'.Какой тип получится в результате применения `Omit<UserProfile, 'id' | 'role'>`, если `UserProfile` содержит поля `id`, `name`, `email`, `role`, `createdAt`?
Не создавайте дженерики ради дженериков:
// ИЗБЫТОЧНО И ПЛОХО:
function printId<T extends string>(id: T): void {
console.log(id);
}
// ПРАВИЛЬНО И ЛАКОНИЧНО:
function printId(id: string): void {
console.log(id);
}Дженерик необходим только тогда, когда между типами нескольких аргументов или типом возвращаемого значения существует логическая связь.
Опасный паттерн, отключающий проверки типов.
Решение: Используйте Record<string, unknown> для неизвестных значений с последующей валидацией, либо строгие типы.