Typescript: Сравнение с Facebook Flow Type System

Созданный на 25 нояб. 2014  ·  31Комментарии  ·  Источник: microsoft/TypeScript

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

Также я не буду говорить об отсутствующих функциях в Flow, поскольку заявленная цель заключается в улучшении TypeScript.
Наконец, эта тема касается только системы типов, а не поддерживаемых функций es6 / es7.

mixed и any

Из документа потока:

  • смешанный: «супертип» всех типов. Любой тип может перетечь в смешанный.
  • любой: «динамический» тип. Любой тип может перетекать в любой, и наоборот.

В основном это означает, что с потоком any является эквивалентом TypeScript any а mixed - эквивалентом TypeScript {} .

Тип Object с потоком

Из потокового документа:

Используйте смешанный, чтобы аннотировать местоположение, которое может принимать что угодно, но не используйте вместо него Object! Сложно рассматривать все как объект, и если вы случайно имеете в виду «любой объект», есть лучший способ указать это, так же как есть способ указать «любую функцию».

С TypeScript Object эквивалентен {} и принимает любой тип, с Flow Object эквивалентен {} но отличается от mixed , он принимает только Object (но не другие примитивные типы, такие как string , number , boolean или function ).

function logObjectKeys(object: Object): void {
  Object.keys(object).forEach(function (key) {
    console.log(key);
  });
}
logObjectKeys({ foo: 'bar' }); // valid with TypeScript and Flow
logObjectKeys(3); // valid with TypeScript, Error with flow

В этом примере параметр logObjectKeys помечен типом Object , для TypeScript это эквивалент {} и поэтому он будет принимать любой тип, например number в случае второго вызова logObjectKeys(3) .
В Flow другие примитивные типы несовместимы с Object поэтому проверка типов сообщит об ошибке при втором вызове logObjectKeys(3) : _number несовместима с Object_.

Тип не равен нулю

Из потокового документа:

В JavaScript null неявно преобразуется во все примитивные типы; это также допустимый обитатель любого типа объекта.
Напротив, Flow рассматривает null как отдельное значение, не являющееся частью какого-либо другого типа.

см. раздел Flow doc

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

var test: string; // error undefined is not compatible with `string`
var test: ?string;
function getLength() {
  return test.length // error Property length cannot be initialized possibly null or undefined value
}

Однако, как и для функции защиты типа TypeScript, поток понимает ненулевую проверку:

var test: ?string;
function getLength() {
  if (test == null) {
    return 0;
  } else {
    return test.length; // no error
  }
}

function getLength2() {
  if (test == null) {
    test = '';
  }
  return test.length; // no error
}

Тип пересечения

см. раздел Flow doc
см. Correspondin TypeScript, выпуск № 1256

Как и поток TypeScript, поддерживающий типы объединения, он также поддерживает новый способ объединения типов: типы пересечений.
С объектом типы пересечения похожи на объявление миксинов:

type A = { foo: string; };
type B = { bar : string; };
type AB = A & B;

AB имеет для типа { foo: string; bar : string;} ;

Для функций это эквивалентно объявлению перегрузки:

type A = () => void & (t: string) => void
var func : A;

эквивалентно :

interface A {
  (): void;
  (t: string): void;
}
var func: A

Захват общего разрешения

Рассмотрим следующий пример TypeScript:

declare function promisify<A,B>(func: (a: A) => B):   (a: A) => Promise<B>;
declare function identity<A>(a: A):  A;

var promisifiedIdentity = promisify(identity);

С TypeScript promisifiedIdentity будет иметь для типа:

(a: {}) => Promise<{}>`.

С потоком promisifiedIdentity будет иметь для типа:

<A>(a: A) => Promise<A>

Вывод типа

Flow вообще пытается вывести больше типов, чем TypeScript.

Вывод параметров

Давайте посмотрим на этот пример:

function logLength(obj) {
  console.log(obj.length);
}
logLength({length: 'hello'});
logLength([]);
logLength("hey");
logLength(3);

С TypeScript не сообщается об ошибках, с потоком последний вызов logLength приведет к ошибке, потому что number не имеет свойства length .

Предполагаемый тип изменяется с использованием

С потоком, если вы явно не введете свою переменную, тип этой переменной изменится с использованием этой переменной:

var x = "5"; // x is inferred as string
console.log(x.length); // ok x is a string and so has a length property
x = 5; // Inferred type is updated to `number`
x *= 5; // valid since x is now a number

В этом примере x изначально имеет тип string , но при назначении числа тип был изменен на number .
С машинописным текстом присвоение x = 5 приведет к ошибке, поскольку x ранее было присвоено string и его тип не может измениться.

Вывод типов Union

Другое отличие заключается в том, что Flow распространяет вывод типа в обратном направлении, чтобы расширить выводимый тип до объединения типов. Этот пример взят из facebook / flow # 67 (комментарий)

class A { x: string; }
class B extends A { y: number; }
class C extends A { z: number; }

function foo() {
    var a = new B();
    if (true) a = new C(); // TypeScript reports an error, because a's type is already too narrow
    a.x; // Flow reports no error, because a's type is correctly inferred to be B | C
}

("правильно" из оригинального сообщения.)
Поскольку поток обнаружил, что переменная a может иметь тип B или C зависимости от условного оператора, теперь она выводится как B | C , и поэтому Оператор a.x не приводит к ошибке, поскольку оба типа имеют свойство x , и если бы мы попытались получить доступ к свойству z возникла бы ошибка.

Это означает, что следующее тоже будет компилироваться.

var x = "5"; // x is inferred as string
if ( true) { x = 5; } // Inferred type is updated to string | number
x.toString(); // Compiles
x += 5; // Compiles. Addition is defined for both string and number after all, although the result is very different

редактировать

  • Обновлены разделы mixed и any , поскольку mixed эквивалентен {} в этом нет необходимости.
  • Добавлен раздел для типа Object .
  • Добавлен раздел о выводе типа

_ Не стесняйтесь уведомлять, если я что-то забыл, я постараюсь обновить проблему. _

Question

Самый полезный комментарий

Лично общий захват и недопустимость обнуления являются целями для высоких значений от Flow. Я прочитаю другую ветку, но я тоже хотел закинуть сюда свой 2с.

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

Я бы аннотировал каждую переменную, которую мог найти, как не допускающую значения NULL в мгновение ока.

Все 31 Комментарий

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

Неожиданные вещи в Flow (я буду обновлять этот комментарий по мере его изучения)

Вывод типа аргумента нечетной функции:

/** Inference of argument typing doesn't seem
    to continue structurally? **/
function fn1(x) { return x * 4; }
fn1('hi'); // Error, expected
fn1(42); // OK

function fn2(x) { return x.length * 4; }
fn2('hi'); // OK
fn2({length: 3}); // OK
fn2({length: 'foo'}); // No error (??)
fn2(42); // Causes error to be reported at function definition, not call (??)

Нет вывода типа из объектных литералов:

var a = { x: 4, y: 2 };
// No error (??)
if(a.z.w) { }

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

Не стесняйтесь. Как я уже сказал, цель состоит в том, чтобы попробовать инвестировать в систему потокового типа, чтобы увидеть, подходят ли некоторые функции для TypeScript one.

@RyanCavanaugh, наверное, последний пример:

var a = { x: 4, y: 2 };
// No error (??)
if(a.z.w) { }

Я сообщу об ошибке, связанной с их алгоритмом нулевой проверки.

Является

type A = () => void & (t: string) => void
var func : A;

Эквивалентно

Declare A : () => void | (t: string) => void
var func : A;

Или это могло быть?

@ Davidhanson90 не совсем:

declare var func: ((t: number) => void) | ((t: string) => void)

func(3); //error
func('hello'); //error

в этом примере поток не может узнать, какой тип в объединении типа func , поэтому он сообщает об ошибке в обоих случаях

declare var func: ((t: number) => void) & ((t: string) => void)

func(3); //no error
func('hello'); //no error

func имеет оба типа, поэтому оба вызова действительны.

Есть ли заметная разница между {} в TypeScript и mixed в Flow?

@RyanCavanaugh Я действительно не знаю, подумав, я думаю, что это почти то же самое, что я все еще думаю об этом.

mixed не имеет свойств, даже свойств, унаследованных от Object.prototype, которые имеет {} (# 1108). Это неверно.

Другое отличие заключается в том, что Flow распространяет вывод типа в обратном направлении, чтобы расширить выводимый тип до объединения типов. Этот пример взят из https://github.com/facebook/flow/issues/67#issuecomment -64221511

class A { x: string; }
class B extends A { y: number; }
class C extends A { z: number; }

function foo() {
    var a = new B();
    if (true) a = new C(); // TypeScript reports an error, because a's type is already too narrow
    a.x; // Flow reports no error, because a's type is correctly inferred to be B | C
}

("правильно" из оригинального сообщения.)

Это означает, что следующее тоже будет компилироваться.

var x = "5"; // x is inferred as string
if ( true) { x = 5; } // Inferred type is updated to string | number
x.toString(); // Compiles
x += 5; // Compiles. Addition is defined for both string and number after all, although the result is very different

Изменить: протестирован второй фрагмент, и он действительно компилируется.
Изменить 2: Как указано ниже в @fdecampredon , if (true) { } вокруг второго назначения требуется, чтобы Flow определил тип как string | number . Без if (true) number вместо этого выводится

Вам нравится такое поведение? Мы пошли по этому пути, когда обсуждали типы объединений, и их значение сомнительно. Тот факт, что система типов теперь имеет возможность моделировать типы с множеством возможных состояний, не означает, что желательно использовать их везде. Якобы вы выбрали язык со средством проверки статического типа, потому что вам нужны ошибки компилятора, когда вы делаете ошибки, а не только потому, что вам нравится писать аннотации типов;) То есть большинство языков выдают ошибку в таком примере (особенно второй) не из-за отсутствия способа моделирования пространства типов, а из-за того, что они действительно считают, что это ошибка кодирования (по схожим причинам многие избегают поддержки множества операций неявного приведения / преобразования).

По той же логике я ожидал такого поведения:

declare function foo<T>(x:T, y: T): T;
var r = foo(1, "a"); // no problem, T is just string|number

но я действительно не хочу такого поведения.

@danquirk Я согласен с вами, что автоматический вывод типа объединения вместо сообщения об ошибке - это не то поведение, которое мне нравится.
Но я думаю, что это исходит из философии потока, больше, чем из реального языка, команда потока пытается создать просто средство проверки типов, их конечная цель - сделать «более безопасный» код без каких-либо аннотаций типов. Это приведет к менее строгим требованиям.

Точная строгость даже спорна, учитывая влияние такого рода поведения. Часто это просто откладывание ошибки (или полное ее скрытие). Наши старые правила вывода типов для аргументов типов в значительной степени отражают аналогичную философию. В случае сомнений мы вывели {} для параметра типа, а не сделали его ошибкой. Это означало, что вы могли делать некоторые глупые вещи и при этом безопасно выполнять некоторый минимальный набор действий (а именно такие вещи, как toString ). Причина в том, что некоторые люди делают глупые вещи в JS, и мы должны попытаться разрешить все, что можем. Но на практике большинство выводов для {} на самом деле были просто ошибками и заставляли вас ждать, пока вы в первый раз не расставите точки над переменной типа T чтобы понять, что это {} (или аналогично неожиданный тип объединения) а затем трассировка в обратном направлении в лучшем случае раздражала. Если вы никогда не расставляли точки (или никогда не возвращали что-то типа T), вы вообще не замечали ошибку до тех пор, пока во время выполнения что-то не взорвалось (или, что еще хуже, поврежденные данные). Так же:

declare function foo(arg: number);
var x = "5";
x = 5;
foo(x); // error

Что за ошибка? Действительно ли x передается foo ? Или это было переназначение x значения совершенно другого типа, чем оно было инициализировано? Как часто люди действительно намеренно выполняют такую ​​повторную инициализацию, а не случайно что-то наступают? В любом случае, определяя тип объединения для x можете ли вы действительно сказать, что система типов была менее строгой в целом, если она все же приводила к (худшей) ошибке? Такой вывод будет менее строгим только в том случае, если вы никогда не сделаете ничего особенно значимого с результирующим типом, что обычно бывает довольно редко.

Возможно, если оставить null и undefined присваиваемыми любому типу, ошибки скрываются таким же образом, большую часть времени переменная, введенная с некоторым типом и скрывающая значение null , приведет к ошибка во время выполнения.

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

Все сводится к тому, ожидает ли кто-то, что var x = new B(); x = new C(); (где B и C оба являются производными от A) должны компилироваться или нет, и если да, то как это следует понимать?

  1. Не должен компилироваться.
  2. Должен компилироваться и выводиться как наиболее производный базовый тип, общий для иерархий типов B и C - A. Для примера числа и строки это будет {}
  3. Должен скомпилироваться и выводиться как B | C .

В настоящее время TS выполняет (1), а Flow - (3). Я предпочитаю (1) и (2) гораздо больше, чем (3).

Я хотел добавить примеры @Arnavion к исходному выпуску, но, немного поиграв, я понял, что вещи более странные, чем то, что мы понимали.
В этом примере:

var x = "5"; // x is inferred as string
x = 5; // x is infered as number now
x.toString(); // Compiles, since number has a toString method
x += 5; // Compiles since x is a number
console.log(x.length) // error x is a number

Сейчас же :

var x = '';
if (true) {
  x = 5;
}

после этого примера x равен string | number
И если я это сделаю:

1. var x = ''; 
2. if (true) {
3.  x = 5;
4. }
5. x*=5;

Я получил сообщение об ошибке в строке 1: myFile.js line 1 string this type is incompatible with myFile.js line 5 number

Мне все еще нужно понять логику здесь ....

Есть еще одна интересная особенность потока, которую я забыл:

function test(t: Object) { }

test('string'); //error

В основном «Object» несовместим с другими примитивными типами, я думаю, что это имеет смысл.

«Захват стандартного разрешения» определенно необходим для TS!

@fdecampredon Да, ты прав. С var x = "5"; x = 5; выводимый тип number . Добавляя if (true) { } вокруг второго присвоения, средство проверки типов обманывается, предполагая, что любое присвоение допустимо, поэтому вместо этого предполагаемый тип обновляется до number | string .

Ошибка, которую вы получаете myFile.js line 1 string this type is incompatible with myFile.js line 5 number , верна, поскольку number | string не поддерживает оператор * (единственные операции, разрешенные для типа объединения, - это пересечение всех операций для всех типов Союз). Чтобы проверить это, вы можете изменить его на x += 5 и вы увидите, что он компилируется.

Я обновил пример в своем комментарии, добавив if (true)

«Захват стандартного разрешения» определенно необходим для TS!

+1

@Arnavion , не знаю, почему вы предпочли бы {} B | C . Вывод B | C расширяет набор программ, выполняющих проверку типов, без ущерба для правильности, что обычно является желательным свойством систем типов.

Пример

declare function foo<T>(x:T, y: T): T;
var r = foo(1, "a"); // no problem, T is just string|number

уже выполняется проверка типов в текущем компиляторе, за исключением того, что T считается {} а не string | number . Это не ставит под угрозу правильность, но в целом менее полезно.

Вывод number | string вместо {} мне не кажется проблемой. В этом конкретном случае он не расширяет набор допустимых программ, однако, если типы разделяют структуру, система типов, понимающая это и делающая несколько допустимых методов и / или свойств, кажется только улучшением.

Вывод B | C расширяет набор программ, которые проверяют тип без ущерба для правильности

Я думаю, что разрешение операции + над чем-то, что может быть строкой или числом, ставит под угрозу правильность, поскольку операции совсем не похожи друг на друга. Это не похоже на ситуацию, когда операция принадлежит общему базовому классу (мой вариант 2) - в этом случае вы можете ожидать некоторого сходства.

Оператор + не может быть вызван, так как он будет иметь две несовместимые перегрузки: в одной оба аргумента являются числами, а в другой - строками. Поскольку B | C уже, чем строка и число, его нельзя использовать в качестве аргумента ни в одной из перегрузок.

За исключением того, что функции двувариантны относительно своих аргументов, так что это может быть проблемой?

Я думал, что, поскольку var foo: string; console.log(foo + 5); console.log(foo + document); компилирует, что оператор string + разрешает что-либо с правой стороны, поэтому string | number будет иметь + <number> как допустимую операцию. Но ты прав:

error TS2365: Operator '+' cannot be applied to types 'string | number' and 'number'.

Многие комментарии были посвящены автоматическому расширению типов в Flow. В обоих случаях вы можете добиться желаемого поведения, добавив аннотацию. В TS вы должны явно расширить при объявлении: var x: number|string = 5; а в Flow вы должны ограничить при объявлении: var x: number = 5; . Я думаю, что случай, который не требует объявления типа, должен использоваться чаще всего. В своих проектах я ожидал, что var x = 5; x = 'five'; будет чаще вызывать ошибку, чем тип объединения. Так что я бы сказал, что Т.С. правильно сделал вывод по этому поводу.

Что касается функций Flow, которые я считаю наиболее ценными?

  1. Ненулевые типы
    Я думаю, что у этого есть очень высокий потенциал уменьшения ошибок. Для совместимости с существующими определениями TS я представляю его больше как ненулевой модификатор string! а не как модификатор потока ?string , допускающий значение NULL. Я вижу в этом три проблемы:
    Как обрабатывать инициализацию членов класса? _ (вероятно, они должны быть назначены в ctor, и если они могут уйти от ctor перед назначением, они считаются допускающими значение NULL) _
    Как обращаться с undefined ? _ (Flow обходит эту проблему) _
    Может ли он работать без большого количества явных объявлений типов?
  2. Разница между mixed и Object .
    Потому что, в отличие от C #, примитивные типы нельзя использовать везде, где есть объект. Попробуйте Object.keys(3) в своем браузере, и вы получите сообщение об ошибке. Но это не критично, поскольку я думаю, что крайних случаев немного.
  3. Захват общего разрешения
    Этот пример имеет смысл. Но я не могу сказать, что пишу много кода, который от этого выиграет. Может быть, это поможет с функциональными библиотеками, такими как Underscore?

Об автоматическом выводе типа объединения: я предполагаю, что "вывод типа" ограничен объявлением типа. Механизм, который неявно подразумевает пропущенное объявление типа. Как := в Go. Я не теоретик типов, но, насколько я понимаю, вывод типа - это проход компилятора, который добавляет явную аннотацию типа к каждому неявному объявлению переменной (или аргументу функции), выведенному из типа выражения, из которого она назначается. Насколько я знаю, вот как это работает ... ну ... с любым другим механизмом вывода типов. C #, Haskell, Go - все они работают так. Или нет?

Я понимаю аргумент о том, чтобы позволить реальному JS использовать семантику TS, но, возможно, это хороший момент, чтобы вместо этого следовать другим языкам. В конце концов, типы - это единственное определяющее различие между JS и TS.

Мне нравятся многие идеи Flux, но эта, ну, если это действительно так ... это просто странно.

Ненулевые типы кажутся обязательной функцией современной системы типов. Было бы легко добавить к ts?

Если вы хотите немного прочитать о сложностях добавления типов, не допускающих значения NULL, в TS, см. Https://github.com/Microsoft/TypeScript/issues/185

Достаточно сказать, что столь же хороши, как и типы, не допускающие значения NULL, подавляющее большинство популярных языков сегодня не имеют типов, не допускающих значение NULL, по умолчанию (именно здесь эта функция действительно сияет) или какой-либо обобщенной функции, не допускающей значения NULL. И лишь немногие, если таковые имеются, пытались добавить его (или успешно добавили) постфактум из-за сложности и того факта, что значительная часть значения непустимости значения NULL заключается в том, что он используется по умолчанию (аналогично неизменяемости). Это не значит, что мы не рассматриваем возможности здесь, но я бы также не назвал это обязательной функцией.

На самом деле, хотя мне не хватает ненулевого типа, реальная функция, которую мне не хватает в потоке, - это общий захват, тот факт, что ts разрешает каждый общий тип в {} очень затрудняет использование некоторых функциональных конструкций, особенно каррирования.

Лично общий захват и недопустимость обнуления являются целями для высоких значений от Flow. Я прочитаю другую ветку, но я тоже хотел закинуть сюда свой 2с.

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

Я бы аннотировал каждую переменную, которую мог найти, как не допускающую значения NULL в мгновение ока.

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

http://sitr.us/2015/05/31/advanced-features-in-flow.html

Была ли эта страница полезной?
0 / 5 - 0 рейтинги