Почему TypeScript не упрощает этот тип пересечения?

В TypeScript можно использовать типы пересечений, чтобы «сузить» тип другого поля:

type Post = {type: "post", body: string}
type User = {type: "user", name: string}
type Entity = Post | User

/* narrows the type 'Entity' to either 'Post' or 'User' */
type PostAgain = Entity & {type: "post"}
type UserAgain = Entity & {type: "user"}

const post: PostAgain = getPost()
const body = post.body /* now, the field 'body' is known to exist in the type */

Но если я хочу, чтобы в этом «сужении» участвовал более сложный тип, я получаю неожиданные результаты.

/* now, the 'type' field involves arrays */
type Post = {type: ["post"], body: string}
type User = {type: ["user"], name: string}
type Entity = Post | User

/* does not work as expected! */
type PostAgain = Entity & {type: ["post"]}
type UserAgain = Entity & {type: ["user"]}

const post: PostAgain = getPost()
const body = post.body /* error here: 'body' is not known to be a field in the type */

Кажется, проблема возникает из-за того, что TypeScript «не видит», что тип Entity & {type: ["post"]} такой же, как Post, хотя (насколько я вижу) для него нет других возможностей.

Я пробовал несколько комбинаций readonly для типов массивов, но без особого успеха.

Из разговора в комментариях кажется, что то, чего я хочу, на данный момент невозможно. Есть ли какая-либо причина, почему? Это неразумно? Не слишком ли это затратно в вычислительном отношении для реализации? Неужели это просто «пока не реализовано», хотя теоретически это может быть? Это слишком сложно реализовать?

TypeScript выполняет упрощение только для различающихся типов объединения. Объединение считается таковым только в том случае, если оно имеет свойство дискриминатора, которое должно иметь литеральный тип. Этот кортеж не в счет.

Bergi 28.08.2024 20:20

Пересечения сводятся к нулю только при определенных обстоятельствах; "post" и "user" являются допустимыми типами дискриминантов, поэтому пересечение уменьшается в соответствии с ms/TS36696#. ["post"] не является допустимым типом дискриминанта (это не буквальный/единичный тип), поэтому пересечения не уменьшаются. Там довольно много вопросов; какие из них первичны?

jcalz 28.08.2024 20:20

Что ж, если его никак нельзя заставить работать, тогда возникает вопрос: «Почему это не работает?» Это неразумно? Можно ли добавить это гарантированное поведение в будущем?

zamfofex 28.08.2024 20:34

Возможно, более конкретно, мне кажется, что тип с полем типа never сам по себе должен быть упрощен до never, хотя я думаю, что это может быть нежелательно, но я не понимаю, почему.

zamfofex 28.08.2024 20:37

Я бы сказал, что авторитетный ответ заключается, прежде всего, в том, что в целом это было бы невозможно с вычислительной точки зрения (хотя если бы были веские основания для конкретных исключений, таких как ms/TS#36696 , это всегда могло бы произойти), согласно комментарии руководителя команды TS на twitter.com/SeaRyanC/status/1326575516250329088 и github.com/microsoft/TypeScript/issues/… . (Есть еще это) Это полностью решает вопрос? Если да, то я напишу подробный ответ. Если нет, то что мне не хватает?

jcalz 28.08.2024 21:04

Кажется, это был бы удовлетворительный ответ. (Хотя это неудовлетворительный вывод!) Вероятно, единственное, о чем я хотел бы получить дополнительные разъяснения, это то, действительно ли ограничение «имеет буквальный тип» (во втором пункте PR, на который вы ссылаетесь) было бы действительно установлено, потому что оно было бы дорогостоящим в вычислительном отношении (или было ли это произвольно для упрощения работы). Твит, на который вы ссылаетесь, не касается этого конкретно, поскольку охватывает гораздо более общий случай.

zamfofex 28.08.2024 21:14
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
6
52
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Общий ответ на вопрос: «Почему TypeScript агрессивно не сокращает пересечения взаимоисключающих типов до никогда?» «это было бы вычислительно невозможно сделать». Может показаться разумным ожидать, что {a: X} & {a: Y} следует уменьшить до never, если X & Y сократится до never. Но это очень плохо масштабируется; если несовместимый тип встречается в каком-то глубоко вложенном месте, то мы попросим компилятор выполнить полную проверку структурного типа для всех типов пересечений, прежде чем продолжить. Некоторые типы являются рекурсивными, поэтому во многих случаях это становится практически невозможным.

Авторитетный ответ см. в этом комментарии к microsoft/TypeScript#57246 руководителя группы разработчиков TS : «Непрактично упрощать все возможные необитаемые типы до never — это требует потенциально неограниченного объема работы. . Поскольку это невозможно, это не инвариант, от которого вы должны пытаться получить зависимость.», или этот твит того же автора , упомянутый в microsoft/TypeScript#50581. Вы можете довольно легко написать тип, который выдает never где-то очень глубоко в объекте, и мы просто не можем ожидать, что TypeScript будет охотиться за ними.


Говоря прагматично, бывают случаи, когда TypeScript намеренно допускает существование таких невозможных пересечений. Канонический — это фирменные примитивы вида string & {prop: number} или number & {xyz: 123}. Очевидно, что эти вещи никогда не будут существовать во время выполнения, но TypeScript делает вид, что они существуют, чтобы позволить вам притвориться, что существует некая различающая структура между идентичными в остальном типами. Агрессивное сокращение количества вещей до never в конечном итоге приведет к нарушению подобных случаев.

Действительно, вы могли бы написать что-то самостоятельно, что действительно существует во время выполнения и хорошо типизировано в соответствии с системой типов, которую TypeScript не сокращает агрессивно:

const x = {
  a: "hello",
  get b(): never { throw new Error("Don't look at this poperty!"); }
}
/* const x: {
    a: number;
    readonly b: never;
} */

Здесь значение x имеет тип, свойство b которого правильно имеет тип never. Но x прекрасно существует, и если вы не пытаетесь прочитать свойство b, вы можете использовать его сколько угодно:

console.info(x.a.toUpperCase()) // "HELLO"

Только если вы зайдете на b, у вас возникнут проблемы:

console.info(x.b) // OH NOEZ

Так нужно ли было x сократить до never? Неясно, какова будет польза от такого сокращения. Конечно, эти странные вещи с never-геттерами могут быть не идиоматическими, поэтому неясно, есть ли огромная польза от предотвращения сокращения, но важно помнить о ситуациях, когда на самом деле могут существовать кажущиеся невозможными типы, поэтому мы будем делать взлом изменения в TypeScript, если мы их реализовали.


Итак, если TypeScript вообще не делает такого сокращения, то почему он это делает, когда конфликтующее свойство представляет собой буквальный тип , подходящий для использования в качестве дискриминантного свойства дискриминируемого объединения?

Что ж, такого поведения на самом деле не было раньше В TypeScript 3.9 было введено сокращение пересечений с помощью дискриминантных свойств , как это реализовано в microsoft/TypeScript#36696.

Это было реализовано как исправление для microsoft/TypeScript#36736 , которое жаловалось на то, что мы не можем использовать пересечения для извлечения членов из дискриминируемых объединений. Сопровождающие языка могли бы сказать «вместо этого используйте Extract<T, U>», но они проверили, что произойдет, если это будет реализовано, и оказалось, что ничего важного не сломалось, поэтому они сохранили его.

Если вы вернетесь к более ранним версиям TypeScript, вы увидите, что даже такие вещи, как "x" & "y", изначально не сводились к never. Сначала их оставили в покое, затем они были сокращены до never при появлении в объединениях , как это реализовано в microsoft/TypeScript#18438 , а затем они в конечном итоге были полностью сокращены до never, как это реализовано в microsoft/TypeScript#. 31838.

Поэтому вполне возможно, что в какой-нибудь будущей версии TypeScript ваш код будет вести себя так, как вы ожидали. Но для того, чтобы это было правдой, необходимо установить, что

  • существует потребность сообщества в таком сокращении
  • это можно сделать без слишком большого дополнительного времени компиляции (поэтому это должен быть поверхностный анализ)
  • это не нарушает реальный код, который каким-то образом зависит от того, не сокращается ли пересечение

Это кажется маловероятным, особенно потому, что ["x"] не так уж широко используется как признак дискриминации, но все возможно!

Детская площадка, ссылка на код

Спасибо! Также спасибо за упоминание Extract<...>, этого было достаточно для моего конкретного случая использования.

zamfofex 28.08.2024 21:56

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