Как отфильтровать массив разных типов с помощью TypeScript и указать явный тип?

У меня есть класс с двумя полями. Эти поля представляют собой массивы разных типов, но у них есть общее поле 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?

TS не может работать с коррелированными объединениями, как описано в ms/TS#30581 ; this[type] = this[type].filter(item => id !== item.id) подходит только в том случае, если вы проверяете его несколько раз, по одному разу для каждого возможного сужения type. Но ТС так не может. Рекомендуемое исправление описано в ms/TS#47109 и включает в себя рефакторинг для общих индексированных доступов и сопоставленных типов, как показано в этой ссылке на игровую площадку. Это относится к q? Если да, то я напишу ответ; если нет, то что мне не хватает?

jcalz 20.04.2023 15:08

Я предполагаю, что да, это связано с корреляционными союзами. Решение выглядит немного странно, но оно действительно работает. Спасибо за пример.

Nikita 21.04.2023 04:25
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
2
81
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ошибка возникает из-за того, что 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 =, компилятор этого не заметит), поэтому вам нужно быть более осторожным.

Площадка ссылка на код

Другие вопросы по теме