Понимание вывода/сужения типа TS с помощью комбинации расширений и реализации

У меня есть следующий пример кода

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?

Это не имеет ничего общего ни с extends, ни с implements, как показано в этой ссылке на игровую площадку. Тем не менее, это определенно сужающая проблема, связанная с интересной ненадежностью системы типов в отношении необязательных свойств. Ваш B не считается более узким, чем Error (то есть Error присваивается B), поэтому сужение как до B, так и до Error приводит к Error, общему подтипу. Но у D есть дополнительное свойство, поэтому D считается более узким, чем Error, поэтому сужение до обоих приводит к D. Без необязательных необоснованных свойств это не имело бы значения, но...

jcalz 19.10.2022 17:23

@jcalz, не могли бы вы дать ответ?

captain-yossarian from Ukraine 19.10.2022 17:24

... с ними вы видите странности, так как Error знает о stack, а D нет. Это полностью отвечает на ваш вопрос или я что-то упустил? Я могу написать полный ответ, если он соответствует вашим потребностям. Иначе чего мне не хватает?

jcalz 19.10.2022 17:24

@captain-yossarianfromUkraine Я сделаю это, если OP подтвердит, в противном случае я сильно рискую написать полный ответ, а затем получить «смеется, нет, извините» в ответ на усилия

jcalz 19.10.2022 17:26

@jcalz ты знаешь, откуда эта несостоятельность? Это то, что можно вывести из документов. Или только из реализации, так что это чистое совпадение, что я наткнулся на это?

Juarrow 19.10.2022 17:30

TypeScript делает вид, что в целях присваиваемости необязательное свойство может быть присвоено неупомянутому свойству. Таким образом, тип {x?: 0, y: 1} взаимно присваивается {y: 1}, а {x?:2, y: 1} также взаимно присваивается {y: 1}. Но {x?:0, y: 1} взаимно не может быть присвоено {x?:2, y: 1}. Это нелогично. Таким образом, в зависимости от порядка операций вы можете заставить компилятор делать забавные вещи с необязательным назначением свойств.

jcalz 19.10.2022 17:33

«Это что-то, что можно вывести и из документов. Или только из реализации, так что это чистое совпадение, что я наткнулся на это?» Это упоминается немного здесь и в выпусках репозитория GitHub ( ms/TS#47499 плюс ссылки оттуда), которые они считают частью «документов», но они не изложены красиво.

jcalz 19.10.2022 17:40

@jcalz Спасибо, поэтому, если вы хотите создать ответ, я отмечу его как ответ.

Juarrow 19.10.2022 17:43

Хорошо, напишу, когда будет возможность. Спасибо!

jcalz 19.10.2022 17:45
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
3
9
51
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Здесь происходит несколько вещей, но я бы сказал, что основная проблема заключается в том, что система типов 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, даже если в нем отсутствует необязательное свойство Errorstack. Когда вы сужаете x с Error до B, компилятор вообще не видит необходимости в сужении. Тип остается Error, и, таким образом, x имеет необязательное свойство stack.

С другой стороны, Error нельзя присвоить D, поскольку D имеет обязательное someProp свойство, которое Error не обязательно имеет. D считается более узким, чем Error. Когда вы сужаете x с Error до D, компилятор послушно сужает до D. И теперь x не имеет известного stack свойства, потому что D не имеет.

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

Площадка ссылка на код

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