26. Generic Constraints
TypeScript: Броня. Урок 25: Generic Constraints (Ограничения дженериков)
Заголовок раздела «TypeScript: Броня. Урок 25: Generic Constraints (Ограничения дженериков)»Generic constraints позволяют ограничить типы, которые могут быть переданы в generic параметр. Вместо того чтобы принимать любой тип, можно указать, что тип должен соответствовать определённым требованиям. Это делает generic функции и классы более безопасными и выразительными, сохраняя их гибкость.
Базовые ограничения с extends
Заголовок раздела «Базовые ограничения с extends»// Без ограничений - принимает любой типfunction identity<T>(value: T): T { return value;}
// С ограничением - только объектыfunction getProperty<T extends object>(obj: T, key: keyof T) { return obj[key];}
const user = { name: 'Alice', age: 30 };const name = getProperty(user, 'name'); // ✓ работает
// const invalid = getProperty('string', 0); // ✗ Ошибка: string не extends object
// Ограничение конкретным типомfunction logLength<T extends { length: number }>(value: T): number { console.log(value.length); return value.length;}
logLength('hello'); // ✓ string имеет lengthlogLength([1, 2, 3]); // ✓ array имеет lengthlogLength({ length: 5 }); // ✓ объект с length// logLength(42); // ✗ number не имеет lengthМножественные ограничения
Заголовок раздела «Множественные ограничения»// Intersection constraintsinterface Named { name: string;}
interface Aged { age: number;}
// T должен иметь И name, И agefunction describe<T extends Named & Aged>(entity: T): string { return `${entity.name} is ${entity.age} years old`;}
const person = { name: 'Alice', age: 30, city: 'London' };console.log(describe(person)); // "Alice is 30 years old"
// const invalid = { name: 'Bob' }; // ✗ Ошибка: отсутствует age// describe(invalid);
// Union constraints (менее полезно)function process<T extends string | number>(value: T): T { // Можно работать только с методами, общими для string и number return value;}Ограничения между параметрами
Заголовок раздела «Ограничения между параметрами»// Один generic ограничен другимfunction getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key];}
const user = { name: 'Alice', age: 30,};
const name = getProperty(user, 'name'); // ✓ stringconst age = getProperty(user, 'age'); // ✓ number// const invalid = getProperty(user, 'city'); // ✗ 'city' не в keyof User
// Более сложный примерfunction setProperty<T, K extends keyof T>( obj: T, key: K, value: T[K]): void { obj[key] = value;}
setProperty(user, 'name', 'Bob'); // ✓setProperty(user, 'age', 31); // ✓// setProperty(user, 'name', 123); // ✗ Ошибка: number не stringПрактический пример: Type-safe Update
Заголовок раздела «Практический пример: Type-safe Update»// Generic функция для частичного обновления объектовfunction updateEntity<T extends object, K extends keyof T>( entity: T, updates: Pick<T, K>): T { return { ...entity, ...updates };}
interface User { id: string; name: string; email: string; age: number; role: 'admin' | 'user';}
const user: User = { id: '1', name: 'Alice', age: 30, role: 'user',};
// Обновление только name и ageconst updated = updateEntity(user, { name: 'Alice Smith', age: 31,});
// TypeScript знает точные типыconst name: string = updated.name;const age: number = updated.age;
// Partial update с ограничениемfunction partialUpdate<T extends object>( entity: T, updates: Partial<T>): T { return { ...entity, ...updates };}
const partiallyUpdated = partialUpdate(user, {});Constructor Constraints
Заголовок раздела «Constructor Constraints»// Ограничение конструкторомinterface Constructable<T> { new (...args: any[]): T;}
function createInstance<T>(Constructor: Constructable<T>, ...args: any[]): T { return new Constructor(...args);}
class User { constructor(public name: string, public age: number) {}}
class Product { constructor(public title: string, public price: number) {}}
const user = createInstance(User, 'Alice', 30);const product = createInstance(Product, 'Laptop', 999);
// С ограничениями на базовый классabstract class Entity { abstract id: string;}
class UserEntity extends Entity { id: string; constructor(public name: string) { super(); this.id = Math.random().toString(36); }}
// Только классы, наследующие Entityfunction createEntity<T extends Entity>( Constructor: new (...args: any[]) => T, ...args: any[]): T { const instance = new Constructor(...args); console.log(`Created entity with ID: ${instance.id}`); return instance;}
const userEntity = createEntity(UserEntity, 'Alice');Жизненный пример: Repository Pattern
Заголовок раздела «Жизненный пример: Repository Pattern»// Generic repository с ограничениямиinterface Entity { id: string;}
interface Timestamped { createdAt: Date; updatedAt: Date;}
// Repository работает только с Entityclass Repository<T extends Entity> { private storage: Map<string, T> = new Map();
async create(data: Omit<T, 'id'>): Promise<T> { const entity = { ...data, id: Math.random().toString(36), } as T;
this.storage.set(entity.id, entity); return entity; }
async findById(id: string): Promise<T | null> { return this.storage.get(id) ?? null; }
async findAll(): Promise<T[]> { return Array.from(this.storage.values()); }
async update(id: string, updates: Partial<T>): Promise<T | null> { const entity = await this.findById(id); if (!entity) return null;
const updated = { ...entity, ...updates }; this.storage.set(id, updated); return updated; }
async delete(id: string): Promise<boolean> { return this.storage.delete(id); }}
// TimestampedRepository только для сущностей с timestampclass TimestampedRepository<T extends Entity & Timestamped> extends Repository<T> { async create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T> { const now = new Date(); const entity = { ...data, id: Math.random().toString(36), createdAt: now, updatedAt: now, } as T;
(this as any).storage.set(entity.id, entity); return entity; }
async update(id: string, updates: Partial<T>): Promise<T | null> { const result = await super.update(id, { ...updates, updatedAt: new Date(), } as Partial<T>);
return result; }
async findRecent(limit: number = 10): Promise<T[]> { const all = await this.findAll(); return all .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) .slice(0, limit); }}
// Определение моделейinterface User extends Entity, Timestamped { name: string; email: string;}
interface Post extends Entity, Timestamped { title: string; content: string; authorId: string;}
// Использованиеconst userRepo = new TimestampedRepository<User>();const postRepo = new TimestampedRepository<Post>();
const user = await userRepo.create({ name: 'Alice',});
const recentUsers = await userRepo.findRecent(5);Recursive Constraints
Заголовок раздела «Recursive Constraints»// Рекурсивные ограниченияtype DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];};
interface Config { server: { host: string; port: number; ssl: { enabled: boolean; cert: string; }; };}
type ImmutableConfig = DeepReadonly<Config>;
// Функция с рекурсивным ограничениемfunction freeze<T extends object>(obj: T): DeepReadonly<T> { Object.freeze(obj);
for (const key in obj) { if (typeof obj[key] === 'object' && obj[key] !== null) { freeze(obj[key]); } }
return obj as DeepReadonly<T>;}
const config: Config = { server: { host: 'localhost', port: 3000, ssl: { enabled: true, cert: '/path/to/cert', }, },};
const frozenConfig = freeze(config);// frozenConfig.server.host = 'new'; // ✗ readonlyFunction Constraints
Заголовок раздела «Function Constraints»// Ограничение функциональными типамиfunction map<T, U>( array: T[], fn: (item: T) => U): U[] { return array.map(fn);}
const numbers = [1, 2, 3, 4, 5];const doubled = map(numbers, x => x * 2);const strings = map(numbers, x => x.toString());
// Ограничение async функцийasync function mapAsync<T, U>( array: T[], fn: (item: T) => Promise<U>): Promise<U[]> { return Promise.all(array.map(fn));}
const urls = ['/api/user/1', '/api/user/2', '/api/user/3'];const users = await mapAsync(urls, async (url) => { const response = await fetch(url); return response.json();});
// Ограничение типа возвратаfunction collect<T, U extends any[]>( items: T[], fn: (item: T) => U): U[] { return items.map(fn);}Union Constraints
Заголовок раздела «Union Constraints»// Ограничение union типовtype Primitive = string | number | boolean | null | undefined;
function isPrimitive<T extends Primitive>(value: T): value is T { const type = typeof value; return ( type === 'string' || type === 'number' || type === 'boolean' || value === null || value === undefined );}
// Ограничение конкретными литераламиtype HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
function request<M extends HttpMethod>( method: M, url: string, body?: M extends 'GET' ? never : any): Promise<Response> { return fetch(url, { method, body: body ? JSON.stringify(body) : undefined, });}
// Type-safe использованиеrequest('GET', '/api/users'); // ✓request('POST', '/api/users', { name: 'Alice' }); // ✓// request('GET', '/api/users', { data: 'invalid' }); // ✗ GET не может иметь bodyDefault Type Parameters с Constraints
Заголовок раздела «Default Type Parameters с Constraints»// Default значение с ограничениемfunction createArray<T extends object = {}>( length: number, factory?: () => T): T[] { const array: T[] = [];
for (let i = 0; i < length; i++) { array.push(factory ? factory() : ({} as T)); }
return array;}
// Использование с defaultconst emptyObjects = createArray(5); // {}[]
// Использование с конкретным типомinterface User { id: number; name: string;}
let userId = 0;const users = createArray<User>(3, () => ({ id: ++userId, name: `User ${userId}`,}));Ключевые моменты
Заголовок раздела «Ключевые моменты»- Generic constraints ограничивают типы через
extends - Можно требовать наличие определённых свойств или методов
- Intersection (
A & B) требует соответствия всем типам - Один generic может быть ограничен другим (
K extends keyof T) - Constructor constraints позволяют работать с классами generic
- Рекурсивные constraints для вложенных структур
- Function constraints для типизации callback’ов
- Комбинируются с conditional types для мощных абстракций
- Используются в Repository pattern, Builder pattern, Factory pattern
- Делают generic код более безопасным без потери гибкости