#typescript #typescript-generics
Вопрос:
Недавно я создал Builder
класс и понял, что некоторые поля должны быть обязательными, прежде чем связывать цепочку построителей с финалом execute()
.
Я решил, что эта проверка может быть выполнена статически во время компиляции, и поэтому я придумал это решение (упрощенное для примера).:
interface abc {
a: string; // required
b: string; // required
c: string; // optional
}
class Builder<T = void> {
protected state: T = {} as any; // necessary ugly cast
public a(aa: string): Builder<T amp; Pick<abc, 'a'>> {
Object.defineProperty(this.state, 'a', {
value: aa,
writable: true,
});
return this as unknown as Builder<T amp; Pick<abc, 'a'>>;
}
public b(bb: string): Builder<T amp; Pick<abc, 'b'>> {
Object.defineProperty(this.state, 'b', {
value: bb,
writable: true,
});
return this as unknown as Builder<T amp; Pick<abc, 'b'>>;
}
public c(cc: string): Builder<T amp; Pick<abc, 'c'>> {
Object.defineProperty(this.state, 'c', {
value: cc,
writable: true,
});
return this as unknown as Builder<T amp; Pick<abc, 'c'>>;
}
public execute(this: Builder<Pick<abc, 'a' | 'b'>>) {
return this.state;
}
}
Теперь, когда я делаю это:
new Builder().a('foo').c('bar').execute();
Я получаю ошибку, которую я желаю , а именно: The 'this' context of type ... is not assignable to method's 'this' of type ...
, что именно то, что я хотел.
Однако, когда я компилирую этот код машинописи и импортирую файлы сборки в другой проект, я больше не получаю эту ошибку.
Это скомпилированное определение типа:
interface abc {
a: string;
b: string;
c: string;
}
declare class Builder<T> {
protected state: T;
a(aa: string): Builder<T amp; Pick<abc, 'a'>>;
b(bb: string): Builder<T amp; Pick<abc, 'b'>>;
c(cc: string): Builder<T amp; Pick<abc, 'c'>>;
execute(this: Builder<Pick<abc, 'a' | 'b'>>): Pick<abc, "a" | "b">;
}
И вот мой tsconfig.json (я исключил пути проекта, чтобы оставаться актуальным для темы):
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"strict": true,
"importHelpers": true,
"declaration": true,
"declarationDir": "dist/esm/types/",
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"noImplicitAny": true,
"lib": [
"ESNext",
"webworker",
"DOM"
]
}
}
Чтобы добавить оскорбление к травме, IntelliSense показывает мне эти типы при наведении указателя мыши на переменные (хотя я знаю, что это не обязательно связано с компилятором).:
new Builder().execute(); // should be an error
// with the type...
Builder<void>.send(this: Builder<Pick<abc, "a" | "b">>): ...
Является ли это намеренным поведением? Нужно ли мне изменять tsconfig.json? Я делаю что-то не так? Если я не скомпилирую класс, то он отлично работает.
Ответ №1:
Вы должны знать, что эта строка НЕБЕЗОПАСНА: protected state: T = {} as any;
.
Пожалуйста, рассмотрите этот подход:
class Builder {
state = {}
public a<Param extends string>(aa: Param) {
Object.assign(this.state, { a: aa })
return this as this amp; { state: { a: Param } }
}
public b<Param extends string>(bb: Param) {
Object.assign(this.state, { b: bb })
return this as this amp; { state: { b: Param } }
}
public c<Param extends string>(cc: Param) {
Object.assign(this.state, { c: cc })
return this as this amp; { state: { c: Param } }
}
public execute() {
return this.state;
}
}
new Builder().a('foo').state.a // foo
new Builder().a('foo').b('bar').state.b // bar
Комментарии:
1. Спасибо! Я не знаю, почему я счел необходимым передать тип в универсальном, когда я мог бы просто сослаться на сам класс, как это сделали вы! Я тоже думал внутри коробки об этой проблеме.
2. Со мной это происходит постоянно
Ответ №2:
С помощью капитана-йоссариана я понял, что пошло не так и почему я столкнулся с проблемой.
Проблема с моим подходом
После двух лет игры с типами у меня в голове возникло неправильное разделение типа и сущности. На самом деле они гораздо более аккуратно переплетены в машинописном тексте. Хотя использование дженериков является отличным подходом для типов с условно-рекурсивным отображением, этот вариант использования был намного проще, как доказал капитан-йоссариан.
Мне не нужно было вставлять тип в общий, просто чтобы постепенно изменять его в каждом последующем вызове. this
это само по себе тип, и это оказалось наиболее полезным здесь.
В конце концов, класс-это просто еще один тип. Поэтому мне не нужен был какой-то внешний интерфейс для сборки, когда я создавал сущность. Я мог бы просто использовать саму сущность для представления «прогрессивного» типа.
(Однако, если у вас есть такой внешний интерфейс, выберите<Тип, ключи><Тип, ключи> отлично подходит для этого варианта использования)
Решение, после применения того, что было предложено
Решение действительно похоже на то, что предложил капитан-йоссариан. Было добавлено несколько строк, чтобы закончить решение моего вопроса.
Я использовал другой интерфейс, потому что это единственная реализация, которую я действительно тестировал. Это также немного легче понять с первого взгляда.
interface ABC {
a: string;
b: string;
c: string;
}
class Builder {
state = {};
public a(aa: string) {
Object.assign(this.state, { a: aa });
return this as this amp; { state: Pick<ABC, 'a'> };
}
public b(bb: Param) {
Object.assign(this.state, { b: bb });
return this as this amp; { state: Pick<ABC, 'b'> };
}
public c(cc: Param) {
Object.assign(this.state, { c: cc });
return this as this amp; { state: Pick<ABC, 'c'> };
}
public execute(this: this amp; { state: Pick<ABC, 'a' | 'b'> }) {
return this.state;
}
}
Хорошая часть заключается в том, что это также работает после того, как оно было скомпилировано. Произошла общая ситуация «победы», но…
Обратная сторона
Вы не можете создать состояние private
, иначе будет создано пересечение типов never
(потому что и this
то , и другое, и ваш интерфейс предоставляют значение state
, и одно из них является частным). Общий подход, хотя и более неуклюжий, хорошо работал с частными полями. В этом смысле мы могли бы просто использовать состояние для обозначения прогресса, но тогда какой смысл проверять тип, если мы не вводим состояния напрямую?
Даже если private
это не сработает, так как результат пересечения типов будет утверждаться как never
, protected
работает просто отлично. Однако это не одно и то же, но если вы планируете вообще скрывать это от общественного пространства, это подойдет.
Что случилось
Этого я действительно не могу сказать. Я уверен, что компилятор не должен выдавать результаты, которые работают иначе, чем когда код не компилируется. Я не уверен, что было потеряно по дороге, но мне любопытно узнать об этом больше. Как и во многих других вещах, возможно, это будет исправлено.
Вывод
Типы должны/могут в основном использоваться для моделирования данных. Это их основное применение, и при правильном использовании это мощный инструмент. Я думаю, что использование расширенных типов для интересных операций полезно, но иногда (как это было в данном случае) выполнение всего механизма таким образом было излишним.