Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Generics в TypeScript: глубокий опыт
Что такое Generics
Generics (обобщённые типы) — это способ написать код, который работает с любым типом, но сохраняет type safety. Это одна из самых мощных фич TypeScript.
Простой пример:
// БЕЗ generics — теряем type информацию
function getFirstElement(arr: any[]): any {
return arr[0];
}
const first = getFirstElement([1, 2, 3]);
// IDE не знает, что first это number!
// С generics — type safety сохранён
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
const first = getFirstElement([1, 2, 3]); // TypeScript знает, что это number
const name = getFirstElement(['Alice', 'Bob']); // TypeScript знает, что это string
1. Generic Functions
Базовые примеры:
// Simple identity function
function identity<T>(value: T): T {
return value;
}
identity<number>(42); // OK
identity<string>('hello'); // OK
identity('hello'); // TypeScript infers тип автоматически
// Multiple type parameters
function swap<A, B>(tuple: [A, B]): [B, A] {
return [tuple[1], tuple[0]];
}
swap<number, string>([1, 'hello']); // [string, number]
// С constraints (ограничениями)
function merge<T extends object>(obj1: T, obj2: T): T {
return { ...obj1, ...obj2 };
}
merge({ name: 'John' }, { age: 30 }); // OK
merge(123, 456); // ОШИБКА! number не extends object
2. Generic Interfaces
Использую часто в API responses:
// Generic interface для API ответов
interface ApiResponse<T> {
data: T;
error: string | null;
timestamp: Date;
status: 'success' | 'error';
}
// Использование
interface User {
id: number;
name: string;
email: string;
}
const getUserResponse: ApiResponse<User> = {
data: { id: 1, name: 'John', email: 'john@example.com' },
error: null,
timestamp: new Date(),
status: 'success'
};
// Для списка пользователей
const listUsersResponse: ApiResponse<User[]> = {
data: [{...}, {...}],
error: null,
timestamp: new Date(),
status: 'success'
};
3. Generic Classes
Пример: Generic Repository
// В production используем это каждый день
interface Entity {
id: number;
createdAt: Date;
}
class Repository<T extends Entity> {
private items: T[] = [];
async find(id: number): Promise<T | null> {
return this.items.find(item => item.id === id) || null;
}
async findAll(): Promise<T[]> {
return this.items;
}
async create(item: T): Promise<T> {
this.items.push(item);
return item;
}
async update(id: number, updates: Partial<T>): Promise<T> {
const item = await this.find(id);
if (!item) throw new Error('Not found');
Object.assign(item, updates);
return item;
}
}
// Использование
interface User extends Entity {
name: string;
email: string;
}
const userRepository = new Repository<User>();
await userRepository.create({
id: 1,
name: 'John',
email: 'john@example.com',
createdAt: new Date()
});
// IDE знает все методы и типы!
const user = await userRepository.find(1);
console.log(user.name); // OK
console.log(user.xyz); // ОШИБКА! нет такого свойства
4. Conditional Types
Очень мощная фишка:
// Условные типы
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<number>; // false
// Более практичный пример:
type Flatten<T> = T extends Array<infer U> ? U : T;
type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number
// Реальный пример из product кода:
type ApiReturnType<T> = T extends Promise<infer U>
? U
: T extends (...args: any[]) => infer R
? R
: T;
async function getUser(id: number): Promise<User> {
return { id, name: 'John', email: 'john@example.com', createdAt: new Date() };
}
type UserType = ApiReturnType<typeof getUser>; // User
5. Generic Utility Types
TypeScript встроенные утилиты:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Partial<T> — все поля опциональны
type UpdateUserInput = Partial<User>; // все поля опциональны
const update: UpdateUserInput = { name: 'Jane' }; // OK
// Required<T> — все поля обязательны
type CompleteUser = Required<User>; // все поля обязательны
// Readonly<T> — все поля readonly
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = {...};
user.name = 'Jane'; // ОШИБКА!
// Pick<T, K> — выбираем только нужные поля
type UserPreview = Pick<User, 'id' | 'name'>;
const preview: UserPreview = { id: 1, name: 'John' }; // OK
const preview: UserPreview = { id: 1, name: 'John', email: 'john@example.com' }; // ОШИБКА!
// Omit<T, K> — исключаем поля
type UserPublic = Omit<User, 'password'>;
// Имеет: id, name, email (password исключён)
// Record<K, T> — объект с конкретными ключами
type RolePermissions = Record<'admin' | 'user' | 'guest', string[]>;
const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read']
};
6. Generic Constraints
Ограничение типов:
// Constraint: T должен быть объектом с свойством 'length'
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength('hello'); // OK (string имеет length)
getLength([1, 2, 3]); // OK (array имеет length)
getLength({ length: 5 }); // OK
getLength(123); // ОШИБКА! number не имеет length
// Constraint на ключи объекта
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'John' };
getProperty(user, 'name'); // OK
getProperty(user, 'xyz'); // ОШИБКА! нет такого свойства
// Constraint на значение параметра
function extractKey<T extends Record<string, any>, K extends keyof T>(
obj: T,
keys: K[]
): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => {
result[key] = obj[key];
});
return result;
}
const extracted = extractKey({ id: 1, name: 'John', age: 30 }, ['id', 'name']);
// extracted имеет тип: { id: number; name: string; }
7. Практический пример: Typed API Client
// Реальный пример из production кода
interface ApiClient {
get<T>(url: string): Promise<T>;
post<T, D>(url: string, data: D): Promise<T>;
put<T, D>(url: string, data: D): Promise<T>;
delete<T>(url: string): Promise<T>;
}
class HttpClient implements ApiClient {
async get<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json() as Promise<T>;
}
async post<T, D>(url: string, data: D): Promise<T> {
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
});
return response.json() as Promise<T>;
}
async put<T, D>(url: string, data: D): Promise<T> {
const response = await fetch(url, {
method: 'PUT',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
});
return response.json() as Promise<T>;
}
async delete<T>(url: string): Promise<T> {
const response = await fetch(url, { method: 'DELETE' });
return response.json() as Promise<T>;
}
}
// Использование
const client = new HttpClient();
interface User {
id: number;
name: string;
email: string;
}
const user = await client.get<User>('/api/users/1');
console.log(user.name); // OK
console.log(user.xyz); // ОШИБКА!
const newUser = await client.post<User, Omit<User, 'id'>>('/api/users', {
name: 'Jane',
email: 'jane@example.com'
});
8. Когда НЕ использовать Generics
// ❌ Переусложнение
function process<T, U, V, W extends T, X extends U>(a: W, b: X): V {
// Too much magic!
}
// ✅ Просто и понятно
function process(user: User, settings: Settings): Result {
// Clear and maintainable
}
// KISS принцип: используй generics когда они действительно нужны
Итого
Generics — это must-have навык для modern TypeScript разработчика:
- Type Safety: IDE и компилятор помогают поймать ошибки
- Reusability: один код работает с разными типами
- Documentation: типы сами документируют код
- Performance: никакого runtime overhead
Мой опыт:
- Использую генерики ежедневно в production коде
- Сложные conditional types и constraints — second nature
- Понимаю как работает TypeScript inference
- Могу быстро разобраться в сложных generic типах