В TypeScript 5.5 .filter() стал лучше сужать тип вывода, однако, похоже, он не сужается правильно при проверке свойства объекта, типизированного объединением.
Рассмотрим следующие два примера (TS Playground):
const example1 = [{ foo: 123 }, { foo: null }]
.filter(item => {
// ^? (parameter) item: { foo: number; } | { foo:…
return item.foo != null;
});
const thisIsOK = example1;
// ^? const thisIsOK: { foo: number; }[]
const example2 = [{ foo: 123 }, { foo: null }]
.map(({ foo }) => ({ foo }))
.filter(item => {
// ^? (parameter) item: { foo: number | null; }
return item.foo != null;
});
const notOK = example2;
// ^? const notOK: { foo: number | null; }[]
Обратите внимание, что в первом примере каждый элемент имеет свой тип (дискриминируемое объединение?). Однако во втором примере мы создали новый объект в .map(), который «объединил» объединение с одним типом и «переместил» его в свойство foo
. Сейчас .filter()
больше не работает. Почему бы и нет и какое решение?
Вы все равно можете помочь этому с помощью явного предиката типа возвращаемого типа.
@zerkms Я вижу, что проблема закрыта, но это все еще не решает проблему? Нужно ли нам по-прежнему использовать защиту типа/предикаты типа? Я посмотрю PR, но сейчас 2 часа ночи.
@jonrsharpe да, возможно. Однако это немного сложнее, когда тип объекта сложный.
Это не работает, поскольку {foo: number | null}
не является дискриминируемым объединением, поэтому проверка свойства foo
не сужает родительский объект; см. ms/TS#42384 . Я не уверен, какое это «решение»; в примере вы просто вручную пишете предикат типа; в противном случае вы можете написать помощника narrowParent
, как показано по ссылке на эту игровую площадку. Это полностью решает вопрос? Если да, то я напишу ответ или найду подходящий источник дублирования; если нет, то что мне не хватает?
Хорошо @jcalz, но мы все равно получаем { foo: number | null; }
как часть родительского союза, возможно ли в итоге получить только { foo: number; }[]
?
(«родительский союз»? Вы имеете в виду «пересечение»?) Вам решать, как написать предикат типа, который соответствует вашим потребностям. Вместо этого вы могли бы написать это вот так. Но варианты использования и крайние случаи необходимо тестировать, несмотря ни на что. Итак, мне продолжить ответ или я все еще что-то упускаю?
Я нашел простое решение, просто вернув разные типы в файле .map()
@jcalz. Я опубликовал это как ответ, но подумал, что ваше предложение также может быть полезным, поэтому я призываю вас опубликовать его, если вы хотите.
Хм, я думал, что .map(({ foo }) => ({ foo }))
было предпосылкой вопроса, я не осознавал, что это то, что вы бы изменили (поскольку теперь вы проверяете foo != null
дважды).
Да, к счастью, в моем случае я мог изменить код, но в тех случаях, когда вы не можете этого сделать, ваше решение определенно подойдет. Хотя вторая проверка на ноль немного неудобна, я думаю, что это хорошее и простое решение, которое я предпочитаю защите типа. Будем надеяться, что в конечном итоге TypeScript достигнет точки, когда .filter()
будет работать без дополнительной гимнастики.
Эту проблему можно решить, вернув тип объединения из .map()
вместо одного типа, где свойство для фильтрации является типом объединения. Спасибо @jcalz за помощь в поиске этого решения.
Решение (TS Playground):
Обратите внимание, как мы возвращаем разные типы из .map()
в зависимости от данных, которые хотим фильтровать.
const example3 = [{ foo: 123 }, { foo: null }]
.map(({ foo }) => foo != null ? { foo } : undefined)
.filter(item => {
// ^? (parameter) item: { foo: number; } | undefi…
return item != null;
});
const thisWorks = example3;
// ^? const thisWorks: { foo: number; }[]
Проблема в том, что, как правило, сужение видимого типа свойства объекта не служит для сужения видимого типа самого объекта, если только объект не относится к типу дискриминируемого объединения и свойство не является дискриминантное свойство. Поэтому, когда вы проверяете свойство foo
объекта {foo: number | null}
, он не сужает объект, тогда как если вы проверяете свойство foo
объекта {foo: number} | {foo: null}
, он сужает (потому что null
является типом единицы и, следовательно, может служить дискриминантом).
На сайте microsoft/TypeScript#42384 уже давно есть открытый запрос на изменение этого поведения. Если это когда-нибудь будет реализовано, то (item: {foo: number | null}) => item.foo != null
будет автоматически выводиться как возвращающий предикат типа, и ваш код просто будет работать.
До тех пор, пока это не произойдет, вам нужно обойти это.
Общий обходной путь — создать специальную функцию защиты типа, которая сужает родительские объекты по свойствам, например:
function narrowParent<T extends object, K extends keyof T, U extends T[K]>(
o: T, k: K, guard: (x: T[K]) => x is U
): o is Extract<{ [P in keyof T]: P extends K ? U : T[P] }, T> {
return guard(o[k])
}
который принимает объект o
типа T
, один из его известных ключей k
типа K
и обратный вызов guard
, который может сужаться от типа o[k]
до некоторого более узкого типа U
и который сужает o
себя до типа, который выглядит как T
кроме поскольку свойство K
сужается. Например:
const x = {
bar: "abc",
baz: Math.random() < 0.5 ? 123 : undefined,
qux: new Date()
}
// const x: { bar: string; baz: number | undefined; qux: Date; }
if (narrowParent(x, "baz", x => x !== undefined)) {
// const x: { bar: string; baz: number; qux: Date; }
}
Вот проверяем x.baz
и оно x
само сужается.
Вооружившись этим, вы можете переписать его как
const example2 = [{ foo: 123 }, { foo: null }]
.map(({ foo }) => ({ foo }))
.filter(item => narrowParent(item, "foo", x => x != null));
const nowOK = example2;
// ^? const nowOK: { foo: number; }[]
Конечно, для какого-то конкретного примера вам может не понадобиться полное общее решение. Вы можете решить, что нужно просто пропустить весь подход «map
-then-filter
» в пользу подхода FlatMap():
const example3 = [{ foo: 123 }, { foo: null }]
.flatMap(({ foo }) => (foo != null ? [{ foo }] : []));
// const example3: { foo: number; }[]