В 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
для типов массивов, но без особого успеха.
Из разговора в комментариях кажется, что то, чего я хочу, на данный момент невозможно. Есть ли какая-либо причина, почему? Это неразумно? Не слишком ли это затратно в вычислительном отношении для реализации? Неужели это просто «пока не реализовано», хотя теоретически это может быть? Это слишком сложно реализовать?
Пересечения сводятся к нулю только при определенных обстоятельствах; "post"
и "user"
являются допустимыми типами дискриминантов, поэтому пересечение уменьшается в соответствии с ms/TS36696#. ["post"]
не является допустимым типом дискриминанта (это не буквальный/единичный тип), поэтому пересечения не уменьшаются. Там довольно много вопросов; какие из них первичны?
Что ж, если его никак нельзя заставить работать, тогда возникает вопрос: «Почему это не работает?» Это неразумно? Можно ли добавить это гарантированное поведение в будущем?
Возможно, более конкретно, мне кажется, что тип с полем типа never
сам по себе должен быть упрощен до never
, хотя я думаю, что это может быть нежелательно, но я не понимаю, почему.
Я бы сказал, что авторитетный ответ заключается, прежде всего, в том, что в целом это было бы невозможно с вычислительной точки зрения (хотя если бы были веские основания для конкретных исключений, таких как ms/TS#36696 , это всегда могло бы произойти), согласно комментарии руководителя команды TS на twitter.com/SeaRyanC/status/1326575516250329088 и github.com/microsoft/TypeScript/issues/… . (Есть еще это) Это полностью решает вопрос? Если да, то я напишу подробный ответ. Если нет, то что мне не хватает?
Кажется, это был бы удовлетворительный ответ. (Хотя это неудовлетворительный вывод!) Вероятно, единственное, о чем я хотел бы получить дополнительные разъяснения, это то, действительно ли ограничение «имеет буквальный тип» (во втором пункте PR, на который вы ссылаетесь) было бы действительно установлено, потому что оно было бы дорогостоящим в вычислительном отношении (или было ли это произвольно для упрощения работы). Твит, на который вы ссылаетесь, не касается этого конкретно, поскольку охватывает гораздо более общий случай.
Общий ответ на вопрос: «Почему 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<...>
, этого было достаточно для моего конкретного случая использования.
TypeScript выполняет упрощение только для различающихся типов объединения. Объединение считается таковым только в том случае, если оно имеет свойство дискриминатора, которое должно иметь литеральный тип. Этот кортеж не в счет.