Сохранить типизацию цепочки fluent-билдера в производном классе
У QueryBuilder есть методы для цепочки. Производный класс добавляет ещё один. Сейчас цепочка рвётся: после первого вызова собственные методы производного класса больше не видны.
Требования: реализуйте where и orderBy так, чтобы new PagedQuery().where("a = 1").limit(10) компилировался как есть — без переаннотирования цепочки, без приведений и без повторного объявления методов в производном классе. build() по-прежнему возвращает собранную строку.
class QueryBuilder {
protected parts: string[] = [];
where(cond: string) {
// ваш код здесь
}
orderBy(col: string) {
// ваш код здесь
}
build(): string {
return this.parts.join(" ");
}
}
class PagedQuery extends QueryBuilder {
limit(n: number): this {
this.parts.push(`LIMIT ${n}`);
return this;
}
}
Допишите реализацию.
Объявите типом возврата this, а не имя класса: where(cond: string): this с return this. Как полиморфный тип this он обозначает класс самого получателя, поэтому limit() производного класса остаётся доступен в цепочке; имя QueryBuilder потеряло бы его на первом звене.
- ✗Аннотировать типом возврата имя класса, из-за чего производный класс теряется посреди цепочки
- ✗Переобъявлять каждый метод цепочки в производном классе, чтобы вернуть ему нужный тип возврата
- ✗Хвататься за самоссылающийся обобщённый базовый класс там, где всё выражает
this
- →Что станет с цепочкой, если один из методов вернёт
new QueryBuilder()вместоthis? - →Как сделать
build()недоступным, покаwhere()не вызван хотя бы раз?
Решение
class QueryBuilder {
protected parts: string[] = [];
where(cond: string): this { // ← не QueryBuilder, а this
this.parts.push(`WHERE ${cond}`);
return this;
}
orderBy(col: string): this {
this.parts.push(`ORDER BY ${col}`);
return this;
}
build(): string {
return this.parts.join(" ");
}
}
class PagedQuery extends QueryBuilder {
limit(n: number): this {
this.parts.push(`LIMIT ${n}`);
return this;
}
}
const sql = new PagedQuery()
.where("a = 1") // this = PagedQuery
.limit(10) // ✅ виден метод производного класса
.orderBy("id") // this = PagedQuery
.build();
Почему this, а не QueryBuilder
this в позиции типа возврата — полиморфный тип this: он обозначает тип получателя в точке вызова. У экземпляра PagedQuery унаследованный where типизируется как (cond: string) => PagedQuery, хотя объявлен в базовом классе один раз.
class Broken {
protected parts: string[] = [];
where(c: string): Broken { this.parts.push(c); return this; } // ← имя класса
}
class BrokenPaged extends Broken {
limit(n: number): this { return this; }
}
new BrokenPaged().where("a").limit(10);
// ~~~~~ Property 'limit' does not exist on type 'Broken'
⚠️ Самоссылающийся параметр class QueryBuilder<T extends QueryBuilder<T>> решает ту же задачу ценой приведения this as unknown as T и заражает сигнатурой каждого потребителя. this даёт то же самое бесплатно.