Сужение типа TypeScript, по-видимому, делает неверное предположение

#typescript

#typescript

Вопрос:

В следующем коде нет ошибок TypeScript, даже несмотря на то, что строка 4 обращается к свойству x , которое не существует.

 type T = {num1: number, num2: number} | {str1: string, str2: string}
let x: T = {num1: 1, num2: 2, str1: 'hello'};
if('str1' in x) {
  console.log(x.str2.toUpperCase());
}
 

Это ошибка с TypeScript или кто-нибудь может указать мне на документацию, объясняющую это поведение? Похоже, что либо TypeScript не должен разрешать назначение в строке 2, либо он не должен предполагать, что 'str1' in x это подразумевает 'str2' in x .

Ответ №1:

С точки зрения чистой обоснованности назначение правильное, а in сужение оператора неверное. С прагматической точки зрения in сужение оператора очень полезно, а назначение — нет. В этих двух функциях есть несоответствие, и поэтому вы представили хороший пример кода TypeScript, для которого ни пуристы типов, ни разработчики идиоматических JS-кодов не были бы особенно довольны поведением языка.


Сначала давайте посмотрим на назначение

 let x: T = {num1: 1, num2: 2, str1: 'hello'};
 

Согласно структурной типизации, инициализатор for x определенно является значением type T , и так оно и есть {num1: 1, num2: 2, str1: true, randomThingy: "x"} . Дополнительные свойства не нарушают тип.

Но TypeScript выполняет проверку избыточных свойств как своего рода правило компоновки, чтобы улавливать ситуации, когда люди выбрасывают информацию о типе. Когда вы присваиваете const x: {a: string} = {a: "", b: 0} , вы отбрасываете информацию о b свойстве, потому что компилятор не может ее восстановить; компилятор b полностью забудет об этом. С другой стороны, const y = {a: "", b: 0}; const x: {a: string} = y; это нормально, потому что, хотя x он и не знает об b этом, y он все равно знает. Правило проверки избыточных свойств — это дань прагматизму, а не безопасности типов.

Это правило иногда заставляет людей думать, что типы объектов в TypeScript не допускают дополнительных свойств и что они являются «точными типами», как указано в microsoft / TypeScript #12936. Но в TypeScript нет точных типов, и правила проверки избыточных свойств срабатывают не везде, где люди думают, что они должны.

Одним из таких случаев является то, когда вы присваиваете типу like T тип объединения, который не является дискриминируемым объединением. Компилятор не разбивает объединение на его члены перед выполнением проверки избыточных свойств. До тех пор, пока свойство ожидается хотя бы в одном члене объединения, компилятор им доволен. В Microsoft / TypeScript # 20863 существует давняя открытая проблема с просьбой изменить это, но в обозримом будущем так оно и есть.

Таким образом, назначение подходит, если вы заботитесь в основном о безопасности типов, но не подходит, если вы заботитесь о точных типах.


Теперь давайте посмотрим на сужение

 if('str1' in x) {
  console.log(x.str2.toUpperCase());
}
 

Это явно неправильное поведение. В конце концов, типы объектов в TypeScript не являются точными, поэтому, если вы знаете, что "str1" это ключ x , вы не можете логически заключить, что "str2" это тоже ключ. Если вы очень заботитесь о безопасности типов, это очень прискорбно.

С другой стороны, это очень полезно. Даже если вы не можете технически сказать, что "str1" это определенно подразумевает наличие "str2" , корреляция между ними, вероятно, очень сильная. Обычно, когда люди проверяют свойства, о которых известно, что они существуют только в одном члене объединения, они делают это, потому что это действительно позволяет им различать объединение на практике, если не в теории.

В microsoft / TypeScript # 15256, запросе на извлечение, в котором реализовано in сужение оператора, есть следующий комментарий ведущего разработчика для команды TS:

Мы рассмотрели аспект надежности, и он не сильно отличается от существующих проблем надежности вокруг объектов с псевдонимами с необъявленными свойствами. … Реальность такова, что большинство объединений уже правильно разделены и не имеют достаточного псевдонима, чтобы выявить проблему. Кто-то, пишущий in тест, не собирается писать «лучшую» проверку; все, что действительно происходит на практике, — это то, что люди добавляют утверждения типа или перемещают код в одинаково необоснованный пользовательский предикат типа. В сети я не думаю, что это хуже, чем статус-кво (и лучше, потому что оно вызывает меньше пользовательских предикатов типа, которые подвержены ошибкам при написании с использованием in из-за отсутствия проверки опечаток).

Таким in образом, сужение оператора — это определенно случай, когда прагматизм побеждает разумность.


И вот оно … прагматически говоря, никто не стал бы присваивать странное значение x , связанное с объединением, и было бы неплохо, если бы компилятор мог предупредить вас, когда вы это сделаете. И, с технической точки зрения, неправильно использовать k in o для обозначения чего-либо, кроме ключа k , и компилятор не должен этого делать.

Однако, отступая назад, это действительно проблема только для пуристов. Система типов TypeScript не является полностью надежной и не предназначена для этого (и попытка сделать это с помощью чего-то, что компилируется в JavaScript, является «дурацким поручением», согласно тому же руководству разработчика). Первая цель разработки TypeScript — выявить возможные ошибки, и, по-видимому, редко бывает, чтобы кто-то случайно выстрелил себе в ногу, как в этом примере.

Тот факт, что одна часть языка способствует надежности, а другая — прагматизму, можно рассматривать как прагматизм языкового дизайна: надежность велика в той степени, в какой она помогает людям писать программы без ошибок, но она будет отброшена в пользу чего-то другого, если это вредит больше, чем помогает. Снова цитирую:

В конечном счете, системы типов — это инструменты, которые мы создали, чтобы помочь нам писать правильные программы. Доказуемая надежность является косвенной мерой для этих усилий. Если ваша программа по своей конструкции соблюдает только правильные типы во время выполнения, то может ли компьютер доказать, что это так, является академическим упражнением в обоих значениях фразы.

И это означает, что если вы хотите увидеть что-то в этом примере, вам придется доказать, что подобные ошибки встречаются чаще, чем думает команда TS. Обычно с примерами реального кода из популярных пакетов, которые с ним сталкиваются.