У fluent API (будівник запитів, конструктор форм, конфігурація) кожен виклик повертає об'єкт для наступного кроку. З генеріками кожен крок може уточнювати тип результату.
Приклад - запит, що знає, які поля вибрано:
type User = { id: number; name: string; email: string; passwordHash: string };
class Query<T extends object, Selected extends keyof T = never> {
constructor(private readonly fields: ReadonlyArray<keyof T> = []) {}
select<K extends keyof T>(...keys: K[]): Query<T, Selected | K> {
return new Query<T, Selected | K>([...this.fields, ...keys]);
}
async get(): Promise<Array<Pick<T, Selected>>> {
/* виконати запит з this.fields */
return [];
}
}
const users = await new Query<User>().select('id').select('name', 'email').get();
// users: Array<{ id: number; name: string; email: string }>
users[0].passwordHash; // помилка: поле не вибрано
new Query<User>().select('nope'); // помилка: такого поля немає
Ключова ідея: генерічний параметр (Selected) - «акумулятор». Кожен метод повертає новий тип з доповненим акумулятором (Selected | K). Через це методи мають повертати новий екземпляр з новим типом, а не this.
Обов'язкові кроки й порядок викликів - так само через параметри-прапорці:
class RequestBuilder<HasUrl extends boolean = false> {
declare private readonly hasUrl: HasUrl; // «фантомне» поле: лише для типів
url(value: string): RequestBuilder<true> { /* ... */ return this as unknown as RequestBuilder<true>; }
send(this: RequestBuilder<true>): Promise<Response> { /* ... */ }
}
new RequestBuilder().send(); // помилка: спочатку url()
new RequestBuilder().url('/api').send(); // ок
Параметр this у методі обмежує, на якому «етапі» метод доступний. Пастка: без поля hasUrl параметр HasUrl ніде не використовується в структурі класу - і через структурну типізацію RequestBuilder<false> вважається сумісним з RequestBuilder<true>, тож заборона мовчки не працює. Генерічний параметр-«прапорець» має бути частиною форми типу.
Де це використовується: Drizzle і Kysely (типізовані SQL-запити), tRPC, Zod (z.object(...).extend(...) накопичує форму), конструктори форм.
Ціна й межі:
- складні типи сповільнюють компілятор і редактор: глибокі ланцюжки з умовними типами на кожному кроці помітно гальмують підказки;
- повідомлення про помилки стають довгими й важкими для читання - типовий біль бібліотек на кшталт ORM;
- для власного коду застосунку часто досить простішого рішення: об'єкт параметрів з точним типом замість ланцюжка викликів.
Такий підхід виправданий у бібліотеках і спільних інструментах, які використовують багато разів, - там вкладення в типи окупається.