免责声明:此问题并非旨在证明Flow比TypeScript更好或更差,我不想批评两个团队的出色工作,而是列出Flow和TypeScript类型系统的差异并尝试评估哪个功能可以改善TypeScript。
另外,我不会谈论Flow中缺少的功能,因为其目的是为了改善TypeScript。
最后,本主题仅涉及类型系统,而不涉及受支持的es6 / es7功能。
mixed
和any
从流文档:
- 混合:所有类型的“超类型”。 任何类型都可以混入。
- 任意:“动态”类型。 任何类型都可以流入任何类型,反之亦然
基本上,这意味着流any
等同于TypeScript any
而mixed
等同于TypeScript {}
。
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视为不属于任何其他类型的唯一值。
请参阅流程文档部分
由于流程文档非常完整,因此我不会详细描述此功能,请记住,它迫使开发人员必须初始化每个变量或将其标记为可为空的示例:
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
}
请参阅流程文档部分
请参阅Correspondin TypeScript问题#1256
像TypeScript流支持联合类型一样,它也支持一种新的组合类型的方式:交集类型。
对于object,交集类型就像声明一个mixins:
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>
流一般尝试推断比TypeScript更多的类型。
让我们看一下这个例子:
function logLength(obj) {
console.log(obj.length);
}
logLength({length: 'hello'});
logLength([]);
logLength("hey");
logLength(3);
使用TypeScript时,不会报告任何错误,由于number
没有length
属性,因此最后一次调用logLength
将导致错误。
使用flow时,除非您明确键入变量,否则此变量的类型将随着该变量的使用而改变:
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
并且其类型无法更改。
另一个区别是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
类型添加了部分。_如果我忘记了任何内容,请随时通知,我将尝试更新此问题。_
这很有趣,也是进行更多讨论的良好起点。 为了我清楚起见,我介意对原帖子进行一些复制编辑更改吗?
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。
@RyanCavanaugh我猜最后一个例子:
var a = { x: 4, y: 2 };
// No error (??)
if(a.z.w) { }
是与他们的null检查算法有关的错误,我将进行报告。
是
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中的{}
和Flow中的mixed
之间是否有明显的区别?
@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我同意您的看法,即自动推断联合类型而不是报告错误不是我喜欢的行为。
但是我认为,这不仅来自流哲学,还在于它不是一门真正的语言,流团队试图创建一种简单的类型检查器,其最终目标是能够制作出没有任何类型注释的“更安全”的代码。 这导致不太严格。
鉴于这种行为的影响,确切的严格性甚至值得商bat。 通常,这只是推迟一个错误(或完全隐藏一个错误)。 我们对类型参数的旧类型推断规则在很大程度上反映了类似的哲学。 如有疑问,我们为类型参数推断{}而不是将其作为错误。 这意味着您可以做一些愚蠢的事情,并且仍然对结果安全地做一些最小的行为(即像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派生),如果期望,应该将其推断为什么?
B | C
。TS当前执行(1),Flow执行(3)。 与(3)相比,我更喜欢(1)和(2)。
我想在最初的问题中添加@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;
我在第一行说了一个错误: myFile.js line 1 string this type is incompatible with myFile.js line 5 number
我仍然需要弄清楚这里的逻辑....
关于流,我还忘了一个有趣的观点:
function test(t: Object) { }
test('string'); //error
基本上,“对象”与其他原始类型不兼容,我认为这是有道理的。
“通用分辨率捕获”绝对是TS的必备功能!
@fdecampredon是的,您是对的。 使用var x = "5"; x = 5;
x的推断类型将更新为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
。 推断B | C
扩展了类型检查的程序集,而又不影响正确性,而afaik是类型系统通常希望的特性。
这个例子
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 | 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';
比工会类型更容易出错。 所以我想TS可以正确地推断出这一点。
至于我认为最有价值的Flow功能?
string!
而不是Flow的可?string
空的修饰符undefined
? _(流程避开了此问题)_mixed
和Object
之间的差异。Object.keys(3)
,您会收到错误消息。 但这并不是至关重要的,因为我认为边缘情况很少。关于自动并集类型推断:我认为“类型推断”仅限于类型声明。 一种隐式推断省略的类型声明的机制。 就像Go中的:=
。 我不是类型理论家,但据我所知,类型推断是编译器通过的过程,它向每个隐式变量声明(或函数参数)添加显式类型注释,该隐式声明是从要为其分配表达式的类型推断出来的。 据我所知,这就是它的工作原理。 C#,Haskell,Go,它们都以这种方式工作。 或不?
我理解有关让现实生活中的JS使用指定的TS语义的论点,但这也许是跟随其他语言的好方法。 毕竟,类型是JS和TS之间唯一的定义差异。
我喜欢很多Flux的想法,但是,如果实际上是这样的话,那很奇怪。
非空类型似乎是现代类型系统的必需功能。 添加到ts容易吗?
如果您想了解向TS添加非空类型的复杂性,请参阅https://github.com/Microsoft/TypeScript/issues/185
可以说,当今绝大多数流行语言都与非可空类型一样好,默认情况下它们都不具有非可空类型(这是该功能真正发挥作用的地方),或者根本没有任何广义的非可空性功能。 由于复杂性和非nullability的大部分价值在于将其作为默认值(类似于不变性),因此几乎没有(如果有的话)尝试添加(或成功添加)。 这并不是说我们不在这里考虑可能性,但我也不会将其称为强制性功能。
实际上,与我错过的非null类型一样,我从流中错过的实际功能是泛型捕获,ts将每个泛型解析为{}
事实使它很难与某些函数构造一起使用,尤其是在curring上。
就个人而言,通用捕获和不可空性是Flow的“高价值目标”。 我将阅读其他主题,但我也想将2c放在这里。
有时我觉得增加非空性的好处几乎不值任何代价。 这是一个非常可能的错误情况,尽管具有默认的为空性现在会削弱内置值,但TypeScript甚至无法通过简单地假设到处都是这种情况来讨论为空性。
我会在心跳中注释所有我发现为不可为空的变量。
流中有很多隐藏功能,未在流的站点中进行记录。 包括SuperType绑定和存在类型
最有用的评论
就个人而言,通用捕获和不可空性是Flow的“高价值目标”。 我将阅读其他主题,但我也想将2c放在这里。
有时我觉得增加非空性的好处几乎不值任何代价。 这是一个非常可能的错误情况,尽管具有默认的为空性现在会削弱内置值,但TypeScript甚至无法通过简单地假设到处都是这种情况来讨论为空性。
我会在心跳中注释所有我发现为不可为空的变量。