У меня есть класс с двумя полями. Эти поля представляют собой массивы разных типов, но у них есть общее поле id
. Итак, я хочу реализовать метод, который фильтрует массив и удаляет элемент по предоставленному идентификатору.
enum ItemType {
VEGETABLES = 'vegetables',
FRUITS = 'fruits'
}
class BaseVegetable {
id: string = '';
color: string = '';
}
class BaseFruit {
id: string = '';
description?: any;
}
class Basket {
vegetables: BaseVegetable[] = [];
fruits: BaseFruit[] = [];
removeItem(id: string, type: ItemType) {
this[type] = this[type].filter(item => id !== item.id);
}
}
Этот пример в TS Playground детская площадка
Теперь я получаю ошибку машинописного текста
Это выражение не вызывается. Каждый элемент объединения типа '{ (предикат: (значение: BaseVegetable, индекс: число, массив: BaseVegetable[]) => значение равно S, thisArg?: any): S[]; (предикат: (значение: BaseVegetable, индекс: число, массив: BaseVegetable[]) => неизвестно, thisArg?: любой): BaseVegetable[]; } | { ...; }' имеет подписи, но ни одна из этих подписей несовместима друг с другом.(2349)
И item
в функции фильтра имеет тип any
.
Хотя, если я напишу что-то вроде const a = this[type]
, я получу правильный тип BaseVegetable[] | BaseFruit[]
.
Не могли бы вы помочь мне понять причину ошибки? Можно ли таким образом реализовать функцию удаления и правильно описать ее с помощью TS?
Я предполагаю, что да, это связано с корреляционными союзами. Решение выглядит немного странно, но оно действительно работает. Спасибо за пример.
Ошибка возникает из-за того, что TypeScript не может следовать корреляциям между несколькими выражениями объединенного типа, как описано в microsoft/TypeScript#30581.
Когда TypeScript анализирует следующий код:
this[type] = this[type].filter(item => id !== item.id)
он видит, что this[type]
— это BaseVegetable[] | BaseFruit[]
, объединение типов массивов, и есть проблемы с вызовом filter
в объединениях (см. microsoft/TypeScript#44373 ). Даже если бы вы могли решить, что он будет видеть только то, что правая сторона — это BaseVegetable[] | BaseFruit[]
, и это может быть неуместным для записи в свойство this
с ключом типа ItemType
, как это предусмотрено в microsoft/TypeScript#30769. Компилятор прав, чтобы помешать людям делать это в целом, как показано здесь:
demo(typeRead: ItemType, typeWrite: ItemType) {
this[typeWrite] = this[typeRead]; // this fails because it's not safe
}
Потому что что, если вы пишете в другое свойство, отличное от того, которое вы читаете? Причина, по которой вы знаете, что ваш код безопасен, заключается в том, что вы используете одинаковую переменную type
на каждой стороне. Но TypeScript здесь не проверяет идентичность, он проверяет тип.
По сути, TypeScript может проверять один блок кода только один раз. Компилятор не спекулятивно сужает type
до ItemType.VEGETABLES
, а затем спекулятивно сужает type
до ItemType.FRUITS
и проверяет каждый случай. Если бы это было так, то компилятор мог бы увидеть, что это работает в обоих случаях. Но это не так, потому что это привело бы к катастрофически плохой производительности компилятора для всех программ, кроме простейших, если бы компилятор автоматически проверял каждый блок кода столько раз, сколько возможно сужений объединения. Возможно, это сработало бы, если бы разработчики могли включить его по мере необходимости, но этот подход был предложен и отклонен в microsoft/TypeScript#25051.
Для устранения ошибки вы можете следовать рекомендациям, описанным в microsoft/TypeScript#47109 . Идея состоит в том, чтобы реорганизовать ваши операции от союзов к дженерикам , потому что дженерики позволяют вам писать один блок кода, который представляет сразу несколько случаев. И компилятор намного лучше анализирует дженерики, особенно универсальные индексированные обращения в отображаемые типы.
Вот как это выглядит для вашего примера:
class Basket {
vegetables: BaseVegetable[] = [];
fruits: BaseFruit[] = [];
removeItem<K extends ItemType>(id: string, type: K) {
const thiz: { [P in K]: Basket[P][number][] } = this; // okay
thiz[type] = thiz[type].filter(item => id !== item.id); // okay
}
}
Мы сделали removeItem
родовым в K
, типа type
. Когда type
появляется в функции несколько раз, компилятор более склонен рассматривать универсальный тип K
как «один и тот же» в левой и правой частях присваивания. Это позволит работать this[type] = this[type];
, но не фильтру.
Чтобы компилятор понял фильтрацию, нам нужно, чтобы this[type]
сам по себе был дженериком вида F<K>[]
для некоторого F
. Мы можем сделать это, переписав this
как отображаемый тип: { [P in K]: Basket[P][number][] }
что означает: каждое свойство this
ключа ItemType
представляет собой массив элементов массива соответствующего свойства Basket
. Это своего рода тавтология. Итак, когда K
есть ItemType
, вы получаете {vegetables: Basket["vegetables"][number][], fruits: Basket["fruits"][number][]}
и, следовательно, {vegetables: BaseVegetable[], fruits: BaseFruit[]}
. Компилятор доволен присвоением this
этому типу, поэтому он позволяет
const thiz: { [P in K]: Basket[P][number][] } = this; // okay
Можно подумать, что это не улучшение, но рефакторинг этого типа позволяет компилятору явно видеть thiz[type]
как единый тип массива, общий для K
:
const thizType: Basket[K][number][] = thiz[Type];
И поэтому вы можете filter()
это и получить Basket[K][number][]
с обеих сторон присваивания, которые принимает компилятор:
thiz[type] = thiz[type].filter(item => id !== item.id); // okay
Таким образом, вы можете исправить это, получив некоторую безопасность типов от компилятора.
Конечно, этот рефакторинг может не стоить вам того. Если вы просто хотите подавить ошибку и двигаться дальше, вы можете использовать утверждения типа, чтобы убедить компилятор в том, что вы делаете правильно (даже если то, как вы убеждаете, это ложь):
removeItem(id: string, type: ItemType) {
this[type] = (this[type] as
{ id: string }[]).filter(item => id !== item.id
) as (BaseVegetable & BaseFruit)[];
}
Это работает; сказать, что this[type]
является {id: string}[]
, более или менее верно, но сказать, что результатом фильтра является (BaseVegetable & BaseFruit)[]
, — ложь. В любом случае компилятор принимает это. Это не будет обнаруживать ошибки (если вы изменили this[type] =
на this.vegetables =
, компилятор этого не заметит), поэтому вам нужно быть более осторожным.
TS не может работать с коррелированными объединениями, как описано в ms/TS#30581 ;
this[type] = this[type].filter(item => id !== item.id)
подходит только в том случае, если вы проверяете его несколько раз, по одному разу для каждого возможного суженияtype
. Но ТС так не может. Рекомендуемое исправление описано в ms/TS#47109 и включает в себя рефакторинг для общих индексированных доступов и сопоставленных типов, как показано в этой ссылке на игровую площадку. Это относится к q? Если да, то я напишу ответ; если нет, то что мне не хватает?