У меня есть следующий пример кода
class B implements Error {
name: string = '';
message: string = '';
// stack?: undefined | string;
}
function Foo(x: any) {
if (x instanceof Error) {
if (x instanceof B) {
x.stack; // works
}
}
}
abstract class C {
someProp: number = 0;
}
class D extends C implements Error{
name: string = '';
message: string = '';
// stack?: undefined | string
}
function Bar(x: any) {
if (x instanceof Error) {
if (x instanceof D) {
x.stack // does not work: Property 'stack' does not exist on type 'D'.
}
}
}
Я действительно не понимаю, почему попытка доступа к stack
в Bar(..)
не удалась, но работает в Foo(..)
.
Почему extends
приводит к сбою для Bar
после сужения до Error во внешнем if?
@jcalz, не могли бы вы дать ответ?
... с ними вы видите странности, так как Error
знает о stack
, а D
нет. Это полностью отвечает на ваш вопрос или я что-то упустил? Я могу написать полный ответ, если он соответствует вашим потребностям. Иначе чего мне не хватает?
@captain-yossarianfromUkraine Я сделаю это, если OP подтвердит, в противном случае я сильно рискую написать полный ответ, а затем получить «смеется, нет, извините» в ответ на усилия
@jcalz ты знаешь, откуда эта несостоятельность? Это то, что можно вывести из документов. Или только из реализации, так что это чистое совпадение, что я наткнулся на это?
TypeScript делает вид, что в целях присваиваемости необязательное свойство может быть присвоено неупомянутому свойству. Таким образом, тип {x?: 0, y: 1}
взаимно присваивается {y: 1}
, а {x?:2, y: 1}
также взаимно присваивается {y: 1}
. Но {x?:0, y: 1}
взаимно не может быть присвоено {x?:2, y: 1}
. Это нелогично. Таким образом, в зависимости от порядка операций вы можете заставить компилятор делать забавные вещи с необязательным назначением свойств.
«Это что-то, что можно вывести и из документов. Или только из реализации, так что это чистое совпадение, что я наткнулся на это?» Это упоминается немного здесь и в выпусках репозитория GitHub ( ms/TS#47499 плюс ссылки оттуда), которые они считают частью «документов», но они не изложены красиво.
@jcalz Спасибо, поэтому, если вы хотите создать ответ, я отмечу его как ответ.
Хорошо, напишу, когда будет возможность. Спасибо!
Здесь происходит несколько вещей, но я бы сказал, что основная проблема заключается в том, что система типов TypeScript не совсем надежна , в частности, в отношении необязательных свойств.
TypeScript позволяет назначать объект без известного свойства типу с этим свойством в качестве необязательного свойства. Итак, {x: string}
можно присвоить {x: string, y?: number}
. Это очень удобно, но не правильно, поскольку все, что компилятор знает о значении типа {x: string}
, это то, что оно имеет x
свойство типа string
. Он ничего не знает о свойстве y
, поэтому он не может знать, что свойство y
является number
или отсутствует. Это известная несостоятельность , упомянутая в списке проблем репозитория TypeScript GitHub в нескольких местах, например, microsoft/TypeScript#47499. Опять же, это удобно; представьте, если бы вы написали const z = {x: "hello"}
, а затем не смогли бы присвоить z
переменной типа {x: string, y?: number}
. Это было бы безопасно, но настолько раздражало бы использование, что люди были бы недовольны.
TypeScript также допускает (относительно) надежную операцию присвоения объекта с известным свойством (необязательным или нет) типу без этого известного свойства. Итак, {x: string, y?: number}
можно присвоить {x: string}
. Типы объектов открытые, не запечатанные. (См. этот вопрос/ответ для получения дополнительной информации.)
Объединение этих типов означает, что типы с необязательными свойствами взаимно назначаемы типам без известных свойств, поэтому, когда компилятор сужает или расширяет один тип до другого, он может в конечном итоге забыть о необязательных свойствах в зависимости от точного набора операций и порядка.
Ваш пример кода эквивалентен
class B {
name: string = '';
message: string = '';
}
function Foo(x: any) {
if (x instanceof Error) {
if (x instanceof B) {
x.stack; // okay
}
}
}
class D {
someProp: number = 0;
name: string = '';
message: string = '';
}
function Bar(x: any) {
if (x instanceof Error) {
if (x instanceof D) {
x.stack // error!
}
}
}
Обратите внимание, что предложения implements
в ваших классах не влияют на типы их экземпляров (см. этот вопрос/ответ для получения дополнительной информации), поэтому вы можете удалить implements Error
, так как он ничего не делает. Точно так же часть extends C
вашего класса служит только для того, чтобы D
наследовал свойство someProp
, поэтому вы можете определить его непосредственно в D
.
Проблема здесь в том, что Error
считается присваиваемым B
, даже если в нем отсутствует необязательное свойство Error
stack
. Когда вы сужаете x
с Error
до B
, компилятор вообще не видит необходимости в сужении. Тип остается Error
, и, таким образом, x
имеет необязательное свойство stack
.
С другой стороны, Error
нельзя присвоить D
, поскольку D
имеет обязательное someProp
свойство, которое Error
не обязательно имеет. D
считается более узким, чем Error
. Когда вы сужаете x
с Error
до D
, компилятор послушно сужает до D
. И теперь x
не имеет известного stack
свойства, потому что D
не имеет.
Так вот. TypeScript имеет некоторую ненадежность, которая разрешена для удобства, и последствия этой ненадежности могут проявляться в некоторых странных местах.
Это не имеет ничего общего ни с
extends
, ни сimplements
, как показано в этой ссылке на игровую площадку. Тем не менее, это определенно сужающая проблема, связанная с интересной ненадежностью системы типов в отношении необязательных свойств. ВашB
не считается более узким, чемError
(то естьError
присваиваетсяB
), поэтому сужение как доB
, так и доError
приводит кError
, общему подтипу. Но уD
есть дополнительное свойство, поэтомуD
считается более узким, чемError
, поэтому сужение до обоих приводит кD
. Без необязательных необоснованных свойств это не имело бы значения, но...