image Примерно полгода назад CEO Reddit Стив сообщил о том, что мы перепроектируем сайт. Главный вопрос тут — как именно мы этим занимаемся. В наше время фронтенд-разработка очень сильно отличается от того, что было во времена, когда Reddit появился на свет. Сейчас имеется огромный выбор вариантов для каждой подсистемы веб-приложения. Как рендерить страницы? Как стилизовать контент? Как хранить и обслуживать картинки и видеофайлы? Как писать код? В современных условиях ни на один из этих вопросов нет готового ответа.

Одним из первых подобных вопросов, на который нам необходимо было найти ответ, звучал так: «Какой язык выбрать?».

О богатстве выбора и требованиях к языку


Как ни странно, нашим языком для фронтенда не обязательно должен был стать JavaScript. В конечном счёте, какой бы язык для этой цели ни был бы выбран, код, написанный на нём, всё равно компилируется в JavaScript. Однако, во что именно компилируется код, возможно, менее важно, чем то, что именно пишет разработчик. Выбор языка оказался делом непростым. Вот что нам пришлось рассмотреть:

  1. BuckleScript
  2. ClojureScript
  3. CoffeeScript
  4. Elm
  5. ElixirScript
  6. JavaScript 2016 и будущие версии языка
  7. JavaScript + аннотации
  8. Nim
  9. PureScript
  10. Reason
  11. TypeScript

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

  1. Это должен быть язык со строгой типизацией. Типы, на микроуровне, играют роль документации. Они помогают, до определённой степени, обеспечивать правильность кода, и, что важнее, упрощают рефакторинг. Ещё одним соображением, в силу которого мы искали язык со строгой типизацией, была скорость разработки. Мы стремились найти строго типизированный язык, так как хотели ускорить работу. Такая идея идёт вразрез с тем видением типизации, которое сложилось у многих программистов. А именно, принято считать, что это — дополнительная нагрузка на разработчика, которая снижает скорость работы. Однако повышение скорости разработки означает ещё и увеличение вероятности появления ошибок. Нам нужна была типизация для того, чтобы код содержал как можно меньше ошибок, даже если пишут его быстро. Строгая типизация, кроме того, полезна в быстрорастущих проектах. Наша команда инженеров постоянно увеличивается в размерах, и число тех, кто работает над кодом, стабильно растёт.

  2. Нужно, чтобы существовали хорошие вспомогательные инструменты. Учитывая то, чем мы собирались заниматься (серьёзное перепроектирование сайта), у нас не было времени на то, чтобы самостоятельно создавать значительное число вспомогательных средств. Было очень важно, чтобы мы могли быстро приступить к работе, используя готовые решения с открытым кодом. Если говорить конкретнее, то вот о каких именно инструментах идёт речь. Это — интеграция с популярными системами сборки (например, с Webpack), поддержка линтера, простая интеграция с фреймворками для тестирования. Если интеграция вспомогательных систем для какого-то языка была неочевидной, мы больше этот язык не рассматривали.

  3. Язык должен был использоваться в серьёзных проектах. Если язык выглядел хорошо, но в реальности использовался лишь в мелких проектах отдельных энтузиастов, он вполне мог оказаться неподходящим для нашей задачи. Кроме того, если язык не используется в серьёзных проектах, возникают вопросы о том, долго ли такой язык просуществует, и о том, как быстро разработчики языка будут реагировать на сообщения о найденных в нём ошибках.

  4. Наши разработчики должны освоить язык достаточно быстро. В вышеприведённом списке есть прекрасные языки, единственный минус которых — слишком большой срок, который нужен разработчикам для того, чтобы их освоить. Среди них хочется отметить Elm и PureScript. Мы серьёзно обсуждали вопрос их использования. Однако, в итоге оказалось, что их внедрение означало бы слишком большой объём работы, необходимый для освоения новых концепций программирования разработчиками, которые с ними не знакомы, для выхода их на уровень, позволяющий продуктивно работать над проектом.

  5. Язык должен работать и на клиенте, и на сервере. На Reddit очень важно SEO, поэтому отсутствие универсальной системы выдачи готовых к отображению в браузере страниц — это большая проблема.

  6. Хорошая поддержка библиотек. Мы не собирались писать всё с нуля. Существуют некоторые задачи, для решения которых нужны надёжные, проверенные временем библиотеки. Нам хотелось, чтобы у нас была возможность выбора того, что нам нужно, из существующих библиотек.

После рассмотрения этих требований мы остановились на двух вариантах. Первый — TypeScript. Второй — JavaScript + Flow. Но, прежде чем сделать окончательный выбор, мы хотели как можно лучше понять особенности TypeScript и Flow, а также различия между ними.

Компиляция или аннотирование?


Одно из важных различий TypeScript и Flow заключается в том, что TypeScript — это язык, который компилируется в JavaScript, а Flow — это набор аннотаций типов, которые можно добавлять к JavaScript-коду. Корректность аннотированного кода проверяет статический анализатор.

Вышеописанные различия прямо влияют на то, как именно пишут программы. Взгляните, например, на работу с перечислениями в TypeScript и Flow.

TypeScript

enum VoteDirection {
  upvoted = 1,
  notvoted = 0,
  downvoted = -1,
};
const voteState: VoteDirection = VoteDirection.upvoted;

Flow

const voteDirections = {
  upvoted: 1,
  notvoted: 0,
  downvoted: -1,
};
type VoteDirection = $Keys<typeof voteDirections>;
const voteState: VoteDirection = voteDirections.upvoted;

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

Однако, у TypeScript, в силу того, что он является компилируемым языком, есть определённые недостатки. При интеграции TypeScript в существующую кодовую базу есть вероятность того, что компилятор усложнит процесс сборки. У нас есть устоявшийся процесс сборки с использованием Babel и слоя транспиляции. Имеется несколько оптимизаций, которые хотелось бы сохранить даже при переходе на TypeScript, поэтому нужен был способ интеграции TypeScript, который не разрушил бы ту схему работы, которая уже есть. Результатом внедрения TypeScript стал бы гораздо более сложный процесс сборки.

В противовес обработке TypeScript, Babel автоматически удаляет аннотации типов Flow. Если бы мы выбрали Flow, процесс сборки приложения не усложнился бы.

Проверка корректности кода


В области проверок корректности кода Flow обычно показывает себя лучше, чем TypeScript. Во Flow, по умолчанию, запрещается использовать типы, допускающие значение NULL. В TypeScript 2.x была добавлена поддержка типов, в которых не допускается NULL, однако, эту возможность нужно включать самостоятельно. Кроме того, Flow лучше выводит типы, в то время как TypeScript часто обращается к типу any.

Помимо типов, допускающих значение NULL и вывода типов, Flow лучше в вопросах ковариантности и контравариантности (вот материал на эту тему). Одна из типичных проблемных ситуаций здесь — работа с типизированными массивами. По умолчанию массивы во Flow инвариантны. Это означает, что следующая конструкция во Flow вызовет ошибку:

Flow

class Animal {}
class Bird extends Animal {}

const foo: Array<Bird> = [];

foo.push(new Animal());
/*
foo.push(new A);
        ^ A. This type is incompatible with
const foo: Array<B> = [];
                ^ B
*/

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

Typescript

class Animal {}
class Bird extends Animal {}

const foo: Array<Bird> = [];

foo.push(new Animal()); // в typescript всё нормально

Можно найти ещё много похожих примеров, однако, общий вывод заключается в том, что Flow гораздо лучше показывает себя в деле проверки типов, нежели TypeScript.

Экосистема


Всё, что было сказано до сих пор, говорит о преимуществах Flow. Его легче интегрировать в проект, он лучше справляется с проверкой типов. Почему же мы остановились на TypeScript?

Одно из важнейших преимуществ TypeScript — его экосистема. Он отличается потрясающей поддержкой библиотек. Практически все библиотеки, которыми мы пользовались, либо имеют описания типов в самих библиотеках, либо представлены в DefinitelyTyped. Кроме того, TypeScript обладает отличной поддержкой IntelliSense в VSCode и в плагинах для других популярных редакторов, которыми мы пользуемся (например, среди них — Atom и SublimeText). Более того, мы обнаружили, что TypeScript умеет обрабатывать аннотации JSDoc. Это оказалось особенно полезным, так как мы переходили на TypeScript с JavaScript.

Кроме того, TypeScript отличается большей «социальной значимостью», и есть ощущение, что срок его жизни будет достаточно долгим. Существует несколько крупных проектов, использующих TypeScript (среди них — VSCode, Rxjs, Angular, да и сам TypeScript), поэтому у нас имеется уверенность в том, что набор его возможностей сможет соответствовать целям нашего проекта, и в том, что язык в ближайшие годы никуда не денется. По поводу же Flow нас беспокоит то, что он был создан для решения специфических задач в Facebook, и то, что его развитие будет определяться тем же диапазоном задач. TypeScript, с другой стороны, ориентирован на широкий спектр проблем, работой над ним занимается Microsoft. Всё это позволяет нам предполагать, что если мы столкнёмся с ошибками и сообщим о них, нас услышат и примут меры.

Кроме того, так как TypeScript — это язык, являющийся надмножеством JavaScript, мы можем ожидать, что развитие TypeScript, в частности, поддержка им новых типов и языковых конструкций, будет следовать за развитием JS. Язык и система типов будут развиваться параллельно, так как работа над ними идёт одновременно.

Итоги


Мы выбрали TypeScript, так как уверены в том, что сможем быстро обучать всему необходимому новых разработчиков (число наших фронтенд-инженеров утроилось за последний год). Мы уверены в том, что язык соответствует нашим запросам в деле перепроектирования сайта. Кроме того, всё говорит о том, что TypeScript будет существовать ещё долго, а наши изыскания показали, что он хорошо интегрируется с нашей кодовой базой. Однако, для нас самое главное это переход на строго типизированный язык.

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

Уважаемые читатели! Пользуетесь ли вы TypeScript или Flow?
Поделиться с друзьями
-->

Комментарии (55)


  1. kvaps
    24.07.2017 14:23
    +9

    Уж кому как ни reddit стоит рассказывать об успехах frontend разработки :)


    1. Boomburum
      24.07.2017 14:31

      Комментарий выглядит как намёк добавить тег irony )


    1. VioletGiraffe
      24.07.2017 15:11
      -4

      Таки да, Reddit — визуальная каша нечитаемая. Лучше б «украли» концепцию у Хабра, тут ориентироваться на порядок проще и приятнее.


      1. Milein
        24.07.2017 16:39
        +2

        В чём ориентироваться-то?
        На хабре пишут статей в день, сколько тем создают в 10 минут в активных сабах реддита.
        И комментариев тут чаще всего не больше пары десятков, редко пара сотен, а на реддите на тысячи счёт идёт бывает.

        Не подумайте, я не заявляю что реддит идеален (иначе бы не было того же RES), но «ориентируйтесь на сайт в котором весь контент за день обычно можно пролистать за 10-20 минут» это несерьёзно.


        1. apro
          24.07.2017 16:47

          В тему frontednt,


          а в reddit действительно нет пред просмотра комментариев или я что-то упускаю?


          1. Milein
            24.07.2017 17:01

            Перед публикацией? Судя по всему нет. Но есть с RES (Reddit Enhancement Suite).


        1. dom1n1k
          24.07.2017 22:35
          -2

          Огромное число раз видел ссылки типа «анонимус запостил нечто на реддит и развернулась активная дискуссия» — перехожу посмотреть, а там 10-20-30 комментариев. Ураган прямо! Один из самых популярных сайтов мира, однозначно. Бывают, конечно, действительно активные топики — но редко.


          1. Milein
            24.07.2017 23:11
            +1

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

            Но вообще да, ~9-ый по популярности сайт в мире, чего ёрничать-то.


    1. justboris
      24.07.2017 18:01

      Мобильная версия у них вроде ничего.


      Но с десктопа на нее не попасть, редиректит на большую.


  1. AstarothAst
    24.07.2017 15:28
    +8

    После прочтения возникает ощущение, что выбор делался примерно так:
    — Flow лучше в этом и в этом, у TypeScript явных преимуществ как бы нет… Но все используют TypeScript, поэтому какого черта?! Ребята, будем использовать TypeScript!


    1. PFight77
      24.07.2017 19:11

      Ну еще тулинг.


    1. Aries_ua
      24.07.2017 21:33

      Просто типа стильно, модно, молодежно.

      Смотрел, TypeScript — не понравилось. Пишем на ES2016. Команде нравится.

      Что касамо типизации для входящих данных, то написал небольшой класс, который типизирует данные для последующей передачи в класс, валидатор или для сохранения. Хватает за глаза и незачем тянуть еще кучу всего. Что касамо чтения кода — то в команде договоренность — писать JSDoc для методов и классов.

      И добавлю что перешли на async/await и код реально упростился и стал очень простым и читаемым.


      1. slavap
        25.07.2017 09:41
        +5

        Что конкретно команде нравится? Не писать типы и вместо них писать JSDoc? Или писать «небольшие» классы с валидаторами на каждый чих? Тянуть кучу всего — это о чём?


      1. raveclassic
        25.07.2017 10:40
        +3

        не понравилось

        А что именно не понравилось? И чем в таком случае выигрывает ES2016?


    1. knotri
      25.07.2017 10:09
      +1

      Как будто хайп это что-то плохое — это означает комьюнити, библиотеки о чем автор в статьи таки сказал.


    1. Rayman2u
      25.07.2017 10:49
      +2

      Я посмотрел Flow и ужаснулся, когда тот начал проверять все библиотеки в node_modules. Сотни ошибок и предупреждений и на тот момент не было решения для этого, кроме того, чтобы все зависимости добавить в исключения => нет типизации для зависимостей.

      Сейчас что то поменялось?


  1. DarkGenius
    24.07.2017 15:38
    +4

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

    Что это за типы, определяемые во время выполнения? Насколько мне известно, типы выводятся на этапе транспиляции.


  1. letchik
    25.07.2017 09:41

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

    TypeScript не компилириуемый, а транслируемый. Кроме того вся информацию о типах на этапе трансляции удаляется. Итоговый JavaScript ничего о типах не знает.


    1. mayorovp
      25.07.2017 09:55

      Одно другому не мешает: компиляция есть разновидность трансляции. Раньше считалось что компиляция — это такая трансляция когда на выходе — машинные коды, но появление JVM, CLR и LLVM значительно размыли понятие компилятора.


      Кроме того вся информацию о типах на этапе трансляции удаляется. Итоговый JavaScript ничего о типах не знает.

      Смотря о каких. Автор же приводит конкретный пример: перечисления. Они остаются в итоговом Javascript.


    1. vintage
      25.07.2017 13:58
      +1

      Знает, через рефлексию. Специальный флаг компилятора позволяет добавлять немного информации о типах в рантайм. Довольно куцую информацию правда.


  1. slavap
    25.07.2017 09:57
    +1

    И неожиданно — если в класс Bird добавить любое свойство, например flying: boolean, то Typescript вполне себе успешно ругается на push. Так что дело не в инвариантности, а в том, что не нужно плодить пустых наследников.


    1. knotri
      25.07.2017 10:14

      Утиная типизация же)


      В typescript учебниках пишут об этом.


    1. raveclassic
      25.07.2017 10:43

      Все верно, просто структурная типизация не сразу в голове укладывается, вот люди и недоумевают.


      1. mayorovp
        25.07.2017 10:57
        +1

        К сожалению, смесь двух видов типизации до добра не доводит:


        class A { foo: string }
        class B { bar: string }
        
        function test(x : A | B) {
          if (x instanceof A) {
            console.log(x.foo);
          } else {
            console.log(x.bar);
          }
        }
        
        test({ foo: "Hello, world" }); // undefined

        Компилятор мог бы заметить ошибку при проверке типов — но не заметил.


        1. raveclassic
          25.07.2017 13:43

          Дело в том, что такие проверки через instanceof бессмысленны, так как проверяется не номинальное наследование, а наличие в цепочке прототипа конструктора. Так как A.prototype не находится в цепочке прототипов {foo: '123'} (что абсолютно верно), то выполняется else-ветка.
          Далее из-за, опять-таки, структурной типизации интерфейс класса A идентичен интерфейсу { foo: string }, поэтому test в состоянии принять такой объект вместо инстанса.
          Решением будет вместо обычного юниона использовать discriminated с общим ключом.


          UPD: Номинальные типы пока на стадии обсуждения


          1. mayorovp
            25.07.2017 13:44

            Решение-то я знаю, но как заставить компилятор выругаться на некорректный код?


            1. raveclassic
              25.07.2017 13:53
              +1

              Можно вот так:


              class A { 
                  private __nominal_A: A; //или что еще покруче
                  foo: string; 
              }

              Тогда в test ничего не запихнуть, кроме new A(), но как по мне лучше перестать пытаться писать на TS как на C#.


            1. vintage
              25.07.2017 17:19

              interface Object { __proto__ : this }
              
              class A extends Object { foo: string }
              class B extends Object { bar: string }
              
              class C { foo: string }
              class D extends Object { foo: string }
              class E extends A {}
              
              function test(x : A | B) {
                if (x instanceof A) {
                  console.log(x.foo);
                } else {
                  console.log(x.bar);
                }
              }
              
              test( new C ); // error
              test( new D ); // ok
              test( new E ); // ok
              test({ foo: "Hello, world" }); // error


              1. mayorovp
                25.07.2017 19:55

                Не проверял, но если ваши примеры верны, то это ничуть не решение получается:


                var d = new D();
                d.foo = "Hello, world!";
                test(d); // undefined


                1. vintage
                  25.07.2017 20:48

                  Так проверьте :-)


                  Да, этот кейс не просекается.


        1. Druu
          25.07.2017 15:16

          > К сожалению, смесь двух видов типизации до добра не доводит:

          Никакой смеси нет. В тайпскрипте только структурный сабтайпинг (на данный момент). В вашем примере по факту, баг occurence typing'а — чекер не должен уметь выводить тип B для х в негативной ветке, только А в позитивной. Впрочем, это могло быть сделано и специально (правильное поведение вынудило бы переписывать ооочень много подобных кейзов).


          1. raveclassic
            25.07.2017 15:38

            Почему не должен? Type narrowing вполне себе работает. Если ввести в юнион третий тип, например


            class A { foo: string }
            class B { bar: string }
            class C { foobar: string }
            
            function test(x : A | B | C) {
              if (x instanceof A) {
                console.log(x.foo);
              } else {
                console.log(x.bar); //error
              }
            }

            то будет ошибка, так как bar не существует в оставшемся юнионе B | C


            1. mayorovp
              25.07.2017 15:44

              От того что ошибку замаскировали, лучше не стало. Потому что на самом деле в ветке else должно было остаться объединение A | B | C.


              1. raveclassic
                25.07.2017 16:06

                С какой стати? instanceof type guards
                Это не ошибка, а ожидаемое поведение при структурной типизации.


                1. mayorovp
                  25.07.2017 16:12
                  +1

                  С такой стати, что из провала проверки instanceof нельзя логически вывести структурное несоответствие типа (что и демонстрирует мой пример).


                  А тот факт, что подобное нелогичное поведение вдруг стало ожидаемым, и говорит о том, что "смесь двух видов типизации до добра не доводит".


            1. Druu
              25.07.2017 16:08
              +1

              > Почему не должен?

              Потому что такая семантика у instanceof. Из x instanceof A следует, что в х есть поля А, но из того факта, что !(x instanceof A) не следует, что в х нет полей А, иными словами, у нас нету никаких корректных утверждений о типе значения в негативной ветке. Мы не можем утверждать, что в негативной ветке у нас тип !A, а значит, и не можем утверждать, что он A | B — A = B. По-этому narrowing должен быть только в позитивных ветках.


              1. Druu
                25.07.2017 16:15

                Собственно, может быть тип C < A | B, с ним код будет валиден в обеих ветках


              1. raveclassic
                25.07.2017 16:25

                Из x instanceof A следует, что в х есть поля А, но из того факта, что !(x instanceof A) не следует, что в х нет полей А

                Из этого следует, что в цепочке прототипов x есть A.prototype, то есть у x есть поля, перечисленные именно в A. В негативную ветку тогда попадает утверждение, что в цепочке прототипов x нет A.prototype, соответственно в x нет и полей, перечисленных именно в A конкретно для этой ветки. Хотя, если юнион включает в себя A и что-то "еще", то при разнице останется это "еще", даже если оно включает в себя поля из A (повторяющиеся).


                Не понимаю, почему это вызывает столько вопросов, объясните, пожалуйста, подробнее?


                1. mayorovp
                  25.07.2017 16:29

                  Посмотрите еще раз на мой пример. Прототипа — нет, а поля там только из A, полей из B — нет.


                  1. raveclassic
                    25.07.2017 16:36

                    Ну так это же другая проблема. instanceof сам по себе не проверяет на структурное соответствие, он проверяет на наличие конструктора в прототипе. Соответствие выводится из того, что у вас юнион состоит из классов и вы используете instanceof, проверяющий как раз конструктор. Но я уже написал, что лучше instanceof вообще не использовать, так как снаружи вам всегда может прилететь объект такой же структуры, но не несущий в прототипе нужный конструктор.


                    1. mayorovp
                      25.07.2017 16:38

                      Так о том и речь. Из положительной проверки instanceof следует структурное соответствие, а из отрицательной не следует ничего. В языке же почему-то из отрицательной проверки следует отсутствие структурного соответствия...


                      1. raveclassic
                        25.07.2017 16:45

                        Эм. Если у меня юнион из 3х классов, значит объект с типом этого юниона содержит в цепочке какой-то из прототитипов этих конструкторов. Если он точно не содержит один из них (instanceof провалился), почему из этого не следует, что в негативной ветке он уже "содержит один из прототипов оставшихся конструкторов"?


                        Я правильно понимаю, что вы ведете к тому, что, будь в TS номинальная типизация, то можно было бы не беспокоиться внутри test о том, что снаружи прилетит объект с той же структурой но без нужного прототипа?


                        1. mayorovp
                          25.07.2017 16:51

                          Я не "веду к этому", я об этом сказал в первом же сообщении.


                          Если мы разрешаем передачу { foo: "Hello, world!" } как значения типа A — то нельзя говорить о том, что в значении типа A | B есть хоть какие-то прототип и конструктор!


                          1. raveclassic
                            25.07.2017 16:55

                            Но ведь это не совсем так… Мы разрешаем передачу объектов как значения интерфейса A, тогда как instanceof проверяет не интерфейс, а конструктор (то есть реальное js-значение — функцию).
                            И, понятно, что мы не можем использовать само значение для указания типа аргумента, только его интерфейс (который просто автоматически выводится).
                            Более того, если вы в юнион вместо классов объедините интерфейсы, вам даже instanceof выполнить не дадут.


                            1. mayorovp
                              25.07.2017 17:04

                              Так в том-то и проблема, что из несоответствия конструктора не следует несоответствие интерфейса!


                              1. raveclassic
                                25.07.2017 17:33

                                Правильно, но инстанс конструктора A имеет интерфейс A, но не всякий объект с интерфейсом A является инстансом конструктора A.
                                Тут проблема несколько иная. x instaceof A вам гарантирует наличие нужных полей, если в цепочке есть прототип A. Более того, можно instanceof использовать на любом object type для любого конструктора, даже не из юниона, тогда тип в ветке условия будет расширен прототипом дополнительного конструктора:


                                class A { foo: string }
                                class B { bar: string }
                                function test(x: B) {
                                  if (x instanceof A) {
                                    x.bar; //ok
                                    x.foo; //ok
                                  }
                                }

                                С другой стороны, интерфейс B (а в аргументах идет именно интерфейс, а не конструктор) не гарантирует наличия конструктора B в прототипе, а лишь гарантирует наличие полей из B где-то, либо на инстансе, либо в прототипе.


                                Из всего этого вытекает, что, даже если вы принимаете в аргументах объект с интерфейсом B, вы не можете гарантировать, что где-то в прототипе этого объекта не сидит конструктор A, и, соответственно, что нельзя применять instanceof A для объекта с интерфейсом B. Именно поэтому тайпчекер разрешает использовать классы не из юниона в instanceof.


                                Но если интерфейс A объединен в юнион с интерфейсом B, то в ветке x instanceof A вы уже рассмотрели интересующий вас случай, когда вам нужны поля из A. А если это произошло, то можно спокойно "вычитать" из юниона поля из A.


                                1. mayorovp
                                  25.07.2017 19:58

                                  Но если интерфейс A объединен в юнион с интерфейсом B, то в ветке x instanceof A вы уже рассмотрели интересующий вас случай, когда вам нужны поля из A. А если это произошло, то можно спокойно "вычитать" из юниона поля из A.

                                  Нет, в ветке x instanceof A был рассмотрен только случай совпадения прототипа. Но остался случай когда поля из A есть — а прототипа A нету.


                                  1. raveclassic
                                    25.07.2017 21:02
                                    -1

                                    Ну так если в аргументах B как выше, то в else и будет только B, так как ситуация, когда прототипа A нет, а поля есть, запрещена типом аргумента.


                                    А если там A | B, то будет вычет, так как A | B подразумевает наличие полей. В этом месте нет проблемы, код корректный и сужение типа тоже корректное.


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


                                    Пока что же, сужение в ветках с instanceof мне видится вполне валидным, так как тип аргумента функции test явно указывает на наличие полей.
                                    Но я согласен, что это неудобно, неочевидно и нужно всегда про это помнить. Поэтому я просто не использую юнионы из классов и instanceof.


                                    1. mayorovp
                                      25.07.2017 21:10

                                      Ну так если в аргументах B как выше, то в else и будет только B, так как ситуация, когда прототипа A нет, а поля есть, запрещена типом аргумента.

                                      В том-то и дело, что такая ситуация не запрещена.


                                      1. raveclassic
                                        25.07.2017 21:24

                                        Для тела функции запрещена. Для вызова нет.


                1. Druu
                  25.07.2017 17:05

                  > Из этого следует, что в цепочке прототипов x есть A.prototype, то есть у x есть поля, перечисленные именно в A.

                  В Тайпскрипте нет такого отношения. A < B => А содержит все поля B. Проверка типов для instanceof не может проверить что-либо кроме этого, потому что на уровне типов тайпскрипта ничто иное невыразимо. По-этому soundness чекер не должен выводить ничего для негативной ветки. Но в тайпскрипте намеренно жертвуют soundness ради удобства и более простой типизации существующего кода и привычных для жс паттернов, так что вполне возможно, что и тут оно — by design.


                  1. raveclassic
                    25.07.2017 17:34

                    Ну как же нет, вон я выше показал пример, когда использование instanceof на дополнительном конструкторе расширяет тип в ветке интерфейсом этого конструктора.


                    1. Druu
                      26.07.2017 01:55
                      -1

                      Не интерфейсом, а _его полями_. В тайпскрипте нету номинальных типов. Нигде нету. И instanceof типизируется структурно (как и все остальное), по наличию полей, а не номинально. Утверждения вида «Х реализует интерфейс А» невозможно выразить на тайпскрипте. На уровне типизации нету никаких классов, интерфейсов и прототипов вообще. Есть только типы, которые либо содержат какие-либо поля, либо не содержат. Единственные утверждения, которые вы можете делать на тайпскрипте: «Х содержит поле Y».


                      1. raveclassic
                        26.07.2017 09:27

                        Вы придираетесь к словам, понятно же, что типизация структурная.
                        Окей, для ветки if (x instanceof A), x содержит все поля интерфейса A. Так лучше?


                        1. Druu
                          29.07.2017 02:49
                          +1

                          > Вы придираетесь к словам, понятно же, что типизация структурная.

                          Это не придирка к словам, это то как работает алгоритм тайпчека (и от этих «придирок» он будет работать с разным результатом)

                          > Окей, для ветки if (x instanceof A), x содержит все поля интерфейса A. Так лучше?

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