Typescript: 建议:不可空类型

创建于 2014-07-22  ·  358评论  ·  资料来源: microsoft/TypeScript

引入两种新的基于 JSDoc 的类型声明语法

var myString: !string = 'hello world'; //non-nullable
var myString1: ?string = 'hello world'; // nullable 
var myString2: string = 'hello world'; // nullable 
var myString3 = 'hello world'; // nullable

默认类型是可为空的。

两个新的编译器标志:

  • inferNonNullableType使编译器推断出不可为空的类型:
var myString3 = 'hello world' // typeof myString is '!string', non-nullable
  • nonNullableTypeByDefault (我想可能有更好的名字):
var myString: !string = 'hello world'; // non-nullable
var myString1: string = 'hello world'; // non-nullable 
var myString2: ?string = 'hello world'; // nullable 
var myString3 = 'hello world' // non-nullable
Committed Fixed Suggestion

最有用的评论

对不起,如果我对一个已关闭的问题发表评论,但我不知道在哪里提问更好,如果没有兴趣,我认为这不值得一个新问题。
在每个文件的基础上处理隐式 null 是否可行?
就像,用 noImplicitNull 处理一堆 td 文件(因为它们来自确定类型并且是那样构思的),但是将我的源作为隐式空处理?
有人会觉得这有用吗?

所有358条评论

我建议使用字符串以外的类型作为示例,因为它本质上是可为空的。 :P
由于“!”的用户和编译器,我可以认为不可为空类型是有问题的。 期望类型始终为非空,这在 JavaScript 中永远无法真正断言。 用户可能会定义如下内容:

function(myNonNull:!myClass):void {
  myNonNull.foo();
}

并且因为它被定义为非空,所以编译器可能会很高兴,但是在javascript中使用它的其他人传递了一些空值和kaboom。

话虽如此,也许解决方案可能是对于面向公众的方法,它可以自动断言不为空。 但是,编译器也可以断言您不能拥有可以具有 !nonnull 声明的公共属性(或实际上是私有的),因为它们无法强制执行。

这可能会深入讨论代码契约,以便正确执行。

原谅我的批评者,我认为如果/尽快代数数据类型在这里,则几乎不需要不可为空的类型。 人们使用null来表示缺失值的原因是因为在 JavaScript 和大多数 OOP 语言中都没有更好的方法来做到这一点。 所以成像 ADT 已经出现了。 然后,至于在不可为空之前编写的旧库,拥有它们不会让生活变得更好。 至于新的库,有了 ADT,就可以根据业务领域规范非常准确地对值可以采用什么进行建模,而根本不使用空值。 所以我想我要说的是 ADT 是解决同一问题的更强大的工具。

就个人而言,我只是写了一个小Maybe<T>接口并使用纪律来确保该类型的变量永远不会为空。

我建议使用字符串以外的类型作为示例,因为它本质上是可为空的。 :P
由于“!”的用户和编译器,我可以认为不可为空类型是有问题的。 期望类型始终为非空,这在 JavaScript 中永远无法真正断言。 用户可能会定义如下内容:

函数(myNonNull:!myClass):void {
myNonNull.foo();
}
并且因为它被定义为非空,所以编译器可能会很高兴,但是在javascript中使用它的其他人传递了一些空值和kaboom。

我真的不明白你也可以定义:

function myFunc(str: string): int {
 return str && str.length;
}

如果有人将int传递给该函数,它也会以错误告终,打字稿的一个优点是委托编译器传递您将在 javascript 中手动检查的内容,再次检查可空/非-nullable 类型对我来说似乎是合理的。 顺便说一下,SaferTypeScript 和 ClosureCompiler 已经做了那种检查。

使用联合类型,我们可以有一个非常简单的规范。
假设我们现在有一个基本类型 'null',我们可以有一个 'stricter' 模式,其中 'null' 和 'undefined' 与任何类型都不兼容,所以如果我们想表达一个可以为 null 的值,我们会这样做:

var myNullableString: (null | string);
var myString = "hello";
myNullableString = myString //valid
myString = myNullableString // error null is not assignable to string;

使用“严格模式”激活打字稿应该检查每个不可为空的变量是否已初始化,默认情况下可选参数也是可以为空的。

var myString: string; // error
var myNullableString: (null | string); // no error

function myString(param1: string, param2?: string) {
  // param1 is string
  // param2 is (null | string)
}

@fdecampredon +1

IIRC 来自 Facebook 展示的 Flow,它使用 TypeScript 语法,但默认情况下使用不可为空的类型,它们支持(null | T)的简写,就像你原来的帖子一样 - 我认为它是?TT?

var myString: string; // error

如果您想有条件地初始化变量,这可能会很烦人,例如:

var myString: string;
if (x) {
myString = a;
} else if (y) {
myString = b;
} else {
myString = c;
}

例如,在 Rust 中,只要编译器可以看到 myString 将在使用之前被初始化,就可以了,但是 TypeScript 的推理目前不支持这一点。

老实说,用var myString = ''而不是var myString: string来做这样的事情并不会打扰我,但可以肯定的是,这种规则总是可能的。

@fdecampredon +1 为此 - 我非常喜欢这个想法。 对于 100% JavaScript 的代码库,这将是一个有用的仅编译时约束。 (据我了解,您的提议无意生成代码来强制执行此操作?)

至于(null | string)简写肯定?string是好的。
当然@johnnyreilly这只是一个编译时检查

默认情况下,Sum 类型使不可为 null 的类型成为一种非常有趣的可能性。 默认情况下不可为空的安全属性不能被夸大。 总和类型加上计划中的“if/typeof 解构”(不确定应该叫什么)甚至使得集成可空和不可空 API 的类型安全。

但是,默认情况下使类型不可为空是一个巨大的突破性更改,这将需要更改几乎所有现有的第三方类型定义文件。 虽然我 100% 支持重大更改,但没有人能够更新野外存在的类型定义。

对这些定义的广泛共识收集在绝对类型存储库中很好,但我仍然对这个功能有实际的担忧。

@samwgoldman的想法是只有在像nonImplicitAny这样的特殊编译器标志下才有不可为空的类型,这个标志可以命名为strictnonNullableType 。 所以不会有重大变化。

@fdecampredon非 TypeScript 库的类型定义怎么样,比如绝对类型的库? 编译器不会检查这些定义,因此任何可能返回 null 的第三方代码都需要重新注释才能正常工作。

我可以想象当前注释为“返回字符串”但有时返回 null 的函数的类型定义。 如果我在nonNullableType 'ed 代码中依赖该函数,编译器不会抱怨(怎么可能?)并且我的代码不再是空安全的。

除非我遗漏了什么,否则我不认为这是可以用标志打开和关闭的功能。 在我看来,这是为了确保互操作性而进行的全有或全无的语义更改。 不过,我很乐意被证明是错误的,因为我认为标志切换功能更有可能发生。

顺便说一句,Facebook 的 Flow 编译器上还没有太多可用信息,但是从演示文稿的视频录制来看,它们似乎默认情况下是不可为空的。 如果是这样,至少这里有一些优先级。

好的,让我们假设type | null | undefined有一个简写? type type | null | undefined

@fdecampredon非 TypeScript 库的类型定义怎么样,比如绝对类型的库? 编译器不会检查这些定义,因此任何可能返回 null 的第三方代码都需要重新注释才能正常工作。

我可以想象当前注释为“返回字符串”但有时返回 null 的函数的类型定义。 如果我在 nonNullableType 代码中依赖该函数,编译器不会抱怨(怎么会?)并且我的代码不再是空安全的。

我没有看到问题,确定一些定义文件在nonNullableType模式下无效,但大多数时候好的库避免返回nullundefined所以在大多数情况下,定义仍然是正确的。
无论如何,我个人很少可以在无需检查/修改的情况下选择一个绝对类型的定义,您只需做一些额外的工作来添加带有某些定义的?前缀。

除非我遗漏了什么,否则我不认为这是可以用标志打开和关闭的功能。 在我看来,这是为了确保互操作性而进行的全有或全无的语义更改。 不过,我很乐意被证明是错误的,因为我认为标志切换功能更有可能发生。

我不明白为什么我们不能有标志切换功能,规则很简单:

  • 在正常模式下? string相当于string并且nullundefined可以分配给所有类型
  • 在 nonNullableType 模式下, ? string等价于string | null | undefined并且nullundefined不能分配给除nullundefined之外的任何其他类型

与标志切换功能的不兼容在哪里?

改变语言语义的标志是一件危险的事情。 一个问题是影响可能非常非局部:

function fn(x: string): number;
function fn(x: number|null): string;

function foo() {
    return fn(null);
}

var x = foo(); // x: number or x: string?

重要的是,查看一段代码的人可以“跟随”类型系统并理解正在做出的推断。 如果我们开始有一堆改变语言规则的标志,这将变得不可能。

唯一安全的做法是保持可赋值性的语义相同,并更改错误与不依赖于标志的错误,就像今天noImplicitAny工作方式一样。

我知道它会破坏复古兼容性,我理解@RyanCavanaugh的观点,但是在使用flowtype品尝之后,它确实是一个非常宝贵的功能,我希望它最终成为打字稿的一部分

除了 RyanCavanaugh 的评论 --> 从我在某处读到的内容中,ES7 规范/提案提到了函数重载的使用(函数名称相同,但输入参数数据类型不同)。 这是 Javascript 非常需要的功能。

流程文档

Flow 将 null 视为不属于任何其他类型的独特值

var o = null;
print(o.x); // Error: Property cannot be accessed on possibly null value

通过编写 ?T 可以使任何类型 T 包含空值(以及未定义的相关值)

var o: ?string = null;
print(o.length); // Error: Property cannot be accessed on possibly null or undefined value

【流程】了解一些动态型式测试的效果

(即在 TS 术语中理解类型守卫)

var o: ?string = null;
if (o == null) {
  o = 'hello';
}
print(o.length); // Okay, because of the null check

限制

  • 由于可能出现别名,对对象属性的检查是有限的:

Flow 除了可以调整局部变量的类型外,有时还可以调整对象属性的类型,尤其是在检查和使用之间没有中间操作的情况下。 但是,一般而言,对象的别名限制了这种推理形式的范围,因为对对象属性的检查可能会因通过别名写入该属性而无效,并且静态分析很难精确跟踪别名

  • 类型保护样式检查对于对象属性可能是多余的。

[D] 不期望在某些方法中可空字段被识别为非空,因为在代码中的其他方法中执行空检查,即使您很清楚空检查足以保证安全运行时(例如,因为您知道对前一种方法的调用总是在对后一种方法的调用之后)。

  • 不检查undefined

未定义的值,就像 null 一样,也会导致问题。 不幸的是,未定义值在 JavaScript 中无处不在,很难在不严重影响语言可用性的情况下避免它们。 例如,数组可以有元素的孔; 可以动态添加和删除对象属性。 Flow 在这种情况下进行了权衡:它检测未定义的局部变量和返回值,但忽略了由于对象属性和数组元素访问而导致未定义的可能性

如果在引入null类型(和问号简写)时同时添加选项怎么办? 文件中null类型的存在将强制编译器进入该文件的不可为空模式,即使该标志不存在于命令行中。 还是这有点太神奇了?

@jbondc看起来不错。 然而,问题在于它最终会以! :p

It's tempting to want to change JavaScript but the reality is a 'string' is nullable or can be undefined.

这是什么意思? js 中没有静态类型。 所以,是的,字符串是“可为空的”,但我们不要忘记它们也是可编号的、可对象的和 fooable 等。任何值都可以有任何类型。

因此,当在 javascript 之上分层静态类型系统时,选择静态类型是否可以为 null 只是一个设计决策。 在我看来,不可为空的类型是更好的默认值,因为通常只在特殊情况下您需要函数签名,例如,除了指定的类型之外还接受空值。

像“use strict”这样导致语义范围变化的指令已经是语言的一部分; 我认为在 TypeScript 中使用“使用不可为空类型”指令是合理的。

@metaweta我认为这还不够,例如,如果 _non null module_ 消耗了一个可空模块会发生什么:

//module A
export function getData(): string[] {
  return null;
}
//module B
'use nonnull'
import A = require('./A');

var data: string[] = A.getData();

模块 B 中的data实际上可以为空,但是由于模块 A 中没有使用'use nonnull'我们应该报告错误吗?
我看不到使用基于指令的功能解决该问题的方法。

是的,

var data: string[] = A.getData();

会导致错误。 相反,您必须为getData()返回null时提供默认值:

var data: string[] = A.getData() || [];

@metaweta好的,但你怎么知道这是一个错误? :)
getData 的类型仍然是 '() => string[]' 您会自动将来自“可空模块”的所有内容视为“可空”吗?

是的,完全正确(除非可空模块中的类型以其他方式显式标记)。

这听起来像您现在想要一个每个文件标志来指示该文件是否默认为可空。

就我个人而言,我认为引入此更改为时已晚,

默认情况下,项目是否以打开或关闭此编译器标志开始? 如果有人正在处理一个无默认可为空的项目并创建/切换到默认可为空的项目,这会引起混淆吗?
我目前在我的大多数项目中都使用 No Implicit Any,每当我遇到一个没有打开该选项的项目时,我都会感到惊讶。

no impicit any 是好的,但就改变语言行为方式的标志而言,我认为这应该是行。 更重要的是,那些从事由不同人以不同规则启动的多个项目的人将由于错误的假设和失误而失去很多生产力。

@RyanCavanaugh担心非局部性,并且指令是词法范围的。 除非您对每个站点进行注释,否则您无法获得更多本地信息。 我不是特别赞成该指令; 我只是指出该选项存在并且它至少与 ES5 中的“使用严格”一样合理。 默认情况下,我个人赞成不可为空的类型,但实际上,为时已晚。 鉴于这些限制,我赞成使用 ! 不知何故。 @jbondc的提议让你区分 null 和 undefined; 鉴于 Java 后端继续让人们同时使用这两个值,这对我来说似乎是最有用的。

如果我不清楚,我很抱歉,我既同意瑞安的观点,又增加了我自己的担忧。

老实说,如果添加use not-null是避免所有空指针异常的代价,我会毫无问题地支付它,考虑nullundefined可分配给任何类型是更糟糕的错误我认为那个打字稿。

@jbondc我没有使用“严格使用”,因此我做了一些假设,如果我的假设有误,请纠正我:

Not null 不会影响程序员编写的语法,但会影响下一个尝试使用该代码的程序员的能力(假设创建者和用户是不同的人)。

所以代码:

function myfoo (mynumber: number) {
    return !!mynumber;
} 

(在手机上打字所以可能是错误的)
在普通项目和非空项目中都是有效代码。 编码人员知道代码是否有效的唯一方法是查看命令行参数。

在工作中,我们有一个测试项目(包括对新功能进行原型设计)和一个主项目(包含我们的实际代码)。 当原型准备好从一个项目移动到另一个项目时(通常使用大型折射镜),代码中不会有错误,但代码的使用会出现错误。 这种行为不同于没有隐式任何和使用严格,两者都会立即出错。

现在我在这些项目中有相当大的影响力,所以我可以警告负责人不要使用这个新的“功能”,因为它不会节省时间,但我没有在所有项目中的能力工作。
如果我们甚至想在一个项目中启用这个功能,那么我们必须在我们所有的其他项目中启用它,因为我们在项目之间有非常大量的代码共享和代码迁移,而这个“功能”会给我们带来很多时间回过头来“修复”已经完成的功能。

正确的@jbondc。 @Griffork :对不起,我没有发现那个误解; 指令是一个文字字符串表达式,它出现在程序产生式或函数产生式的第一行,其作用范围仅限于该产生式。

"use not-null";
// All types in this program production (essentially a single file) are not null

相对

function f(n: number) {
  "use not-null";
  // n is not null and local variables are not null
  function g(s: string) {
    // s is not null because g is defined in the scope of f
    return s.length;
  }
  return n.toFixed(2);
}

function h(n: number) {
  // n may be null
  if (n) { return n.toFixed(3); }
  else { return null; }
}

不可为空的类型是无用的。 不可为空的类型是有用的。 他们没用。 无用! 你没有意识到,但你并不真正需要它们。 宣布从现在开始我们将不再使用 NULL 来限制自己是没有意义的。 您将如何表示缺失值,例如在您尝试查找不存在的子字符串的情况下? 无法表达缺失值(现在 NULL 所做的)不会解决您的问题。 您将用 NULL 到处都是严酷的世界,换来同样严酷且完全没有缺失值的世界。 您真正需要的是代数数据类型(在许多其他很酷的东西中)具有表示缺失值的能力(您首先要查找的内容以及在命令式世界中由 NULL 表示的内容)。 我强烈反对在语言

@aleksey-bykov 听起来您不知道 JavaScript 有两个无效值, undefinednull 。 JavaScript 中的null值仅在不匹配的正则表达式和在 JSON 中序列化日期时返回。 它出现在语言中的唯一原因是为了与 Java 小程序交互。 已声明但未初始化的变量是undefined ,而不是null 。 对象上缺少属性返回undefined ,而不是null 。 如果您明确希望undefined成为有效值,那么您可以测试propName in obj 。 如果要检查对象本身是否存在某个属性而不是它是否被继承,请使用obj.hasOwnProperty(propName) 。 缺少子串返回 -1: 'abc'.indexOf('d') === -1

在 Haskell 中,Maybe 之所以有用,正是因为没有通用子类型。 Haskell 的底部类型代表非终止,而不是通用子类型。 我同意需要代数数据类型,但如果我想要一个用整数标记的树,我希望每个节点都有一个整数,而不是nullundefined 。 如果我想要这些,我将使用标记为 Maybe int 或拉链的树。

如果我们采用“use not-null”指令,我也喜欢“use not-void”(既不是空也不是未定义)。

如果你想从空值保证你自己的代码只是禁止空值
文字。 这比开发不可为空的类型要容易得多。 未定义是一个
稍微复杂一点,但如果你知道它们来自哪里
你知道如何避免它们。 Haskell 的底部是无价的! 我希望
JavaScript (TypeScript) 有一个没有值的全局超类型。 我想念它
当我需要表达时很糟糕。 我一直在使用 TypeScript
因为 v 0.8 并且从未使用过空值,更不用说需要它们了。 直接无视(好了
它们就像您使用任何其他无用的语言功能一样,例如with
陈述。

@aleksey-bykov 如果我正在编写一个库并想保证输入不为空,我必须在任何地方对其进行运行时测试。 我想要它的编译时测试,它不难提供,Closure 和 Flow 都提供对非空/未定义类型的支持。

@metaweta ,您不能保证自己不会为空。 在您的代码编译之前,有无数种方法可以让您的库哭泣: pleaseNonNullablesNumbersOnly(<any> null) 。 编译成js后就没有规则可言了。 其次,你为什么要关心? 预先大声清楚地说出nulls are not supported, you put a null you will get a crash ,就像免责声明一样,您不能保证自己免受外面各种人的影响,但您可以概述您的职责范围。 第三,我很难想到一个主要的主流库,它对任何用户输入的输入都是无懈可击的,但它仍然非常流行。 那么你的努力值得麻烦吗?

@aleksey-bykov 如果我图书馆的客户也接受了类型检查,那么我当然可以保证我不会得到空值。 这就是 TypeScript 的全部意义所在。 根据您的推理,根本不需要类型:只需在文档中“大声说清楚”期望的类型是什么。

题外话,空值对我们来说非常有价值,因为检查它们比检查 undefined 更快。
虽然我们不会在任何地方使用它们,但我们尝试在可能的情况下使用它们来表示未初始化的值和缺失的数字。

主题:
我们从来没有遇到过空值“转义”到其他代码中的问题,但是我们遇到了随机未定义或 NaN 出现的问题。 我相信在这种情况下,仔细的代码管理比标记更好。

然而,对于库类型,拥有冗余类型 null 会很好,这样我们就可以选择注释可以返回 null 的函数(这不应该由编译器强制执行,而是由编码实践强制执行)。

@metaweta ,根据我的推理,您的客户不应在其代码库中使用空值,这并不难,请对空值(区分大小写,整个单词)进行完整搜索并删除所有这些值。 太笨重? 添加一个编译器开关--noNullLiteral以增加花哨的效果。 其他一切都保持不变,相同的代码,不用担心,以最小的占用空间更轻的解决方案。 回到我的观点,假设您的不可空类型找到了 TS 并以两种不同的方式访问:

  • 可以使用!语法来表示不能为空的类型,例如string!不能为空
  • noNullsAllowed 开关打开

然后你从你的服务器通过 ajax 得到一段 json,到处都是空值,道德:javascript 的动态特性不能通过它上面的类型注释来修复

@aleksey-bykov 同样,如果我期待一个具有数字属性x并且我从服务器获得{"x":"foo"} ,类型系统将无法阻止它. 在服务器上使用 TypeScript 以外的东西时,这必然是一个运行时错误和不可避免的问题。

但是,如果服务器是用 TypeScript 编写并在节点上运行的,那么它可以在存在用于我的前端代码的 .d.ts 文件的情况下进行转换,并且类型检查将保证服务器永远不会发送带有空值的 JSON在它或x属性不是数字的对象中。

@metaweta ,不可空类型肯定是另一种类型安全措施,我毫不怀疑,我是说通过强加一些非常基本的规则(避免代码中的空文字),您可以消除 90% 的问题而无需要求来自编译器的任何帮助。 好吧,即使您有足够的资源来执行此措施,您仍然无法消除其余 10% 的问题。 那么问题到底是什么呢? 我问:我们真的需要它吗? 我没有,我成功地学会了如何在没有空值的情况下生活(问我如何),我不记得上次我什么时候收到空引用异常(除了来自我们服务器的数据)。 我希望我们有更酷的东西。 这个特殊的人是如此微不足道。

是的,我们确实非常需要这个。 参见霍尔的十亿美元错误。 空指针异常 (NPE) 是类型化编程语言中遇到的最常见错误,这些语言不区分可空类型和不可空类型。 它是如此普遍,以至于 Java 8 添加了Optional以拼命地与它作斗争。

在类型系统中建模可空值不仅仅是一个理论上的问题,它是一个巨大的改进。 即使您非常小心地避免代码中的空值,您使用的库也可能不会,因此能够使用类型系统正确建模它们的数据很有用。

既然 TS 中有联合和类型保护,类型系统就足够强大了。 问题是它是否可以以向后兼容的方式完成。 我个人觉得这个特性对于 TypeScript 2.0 在这方面向后不兼容来说非常重要。

正确实现此功能可能会指向已经损坏的代码而不是破坏现有代码:它只会指向在其外部泄漏空值的函数(很可能是无意的)或未正确初始化其成员的类(这部分更难,因为类型系统可能需要考虑在构造函数中初始化成员值)。

这与_不使用_空值无关。 它关于正确建模所有涉及的类型。 事实上,这个特性将允许以安全的方式使用空值——没有理由再避免它们了! 最终结果将与代数Maybe类型上的模式匹配非常相似(除了它使用 if 检查而不是 case 表达式完成)

这不仅仅是关于空文字。 nullundefined在结构上是相同的(afaik 没有函数/操作符可以在一个而不是另一个上工作),因此它们可以用 TS 中的单个空类型充分建模。

@metaweta ,

JavaScript 中的 null 值仅在不匹配的正则表达式和在 JSON 中序列化日期时返回。

根本不是真的。

  • 与 DOM 的交互产生 null:
  console.log(window.document.getElementById('nonExistentElement')); // null
  • 正如@aleksey-bykov 上面指出的,ajax 操作可以返回 null。 实际上undefined不是有效的 JSON 值:
 JSON.parse(undefined); // error
 JSON.parse(null); // okay
 JSON.stringify({ "foo" : undefined}); // "{}"
 JSON.stringify({ "foo" : null}); // '{"foo":null}'

注意:我们可以假设undefined是通过 ajax 返回的,因为访问一个不存在的属性将导致undefined - 这就是未定义未序列化的原因。

但是,如果服务器是用 TypeScript 编写并在节点上运行的,那么它可以在存在用于我的前端代码的 .d.ts 文件的情况下进行转换,并且类型检查将保证服务器永远不会发送带有空值的 JSON在它或 x 属性不是数字的对象中。

这并不完全正确。 即使服务器是用 TypeScipt 编写的,如果不检查从持久存储中获得的每个对象的每个属性,就无法保证引入空值。

我有点同意@aleksey-bykov 的看法。 如果我们可以让 TypeScript 在编译时就 null 和 undefined 引入的错误向我们发出警报,那将是非常棒的,但我担心它只会引起错误的自信感,最终会捕捉到琐事,而真正的 null 来源却未被发现。

即使服务器是用 TypeScipt 编写的,如果不检查从持久存储中获得的每个对象的每个属性,就无法保证引入空值。

这实际上是一个参数 _for_ 不可空类型。 如果您的存储可以返回空的 Foo,那么从该存储中检索到的对象的类型是 Nullable<Foo>,而不是 Foo。 如果您有一个返回 Foo 的函数,那么您_必须_通过处理空值来承担责任(要么因为您更了解而强制转换它,要么检查空值)。

如果您没有不可为空的类型,您就不必在返回存储的对象时检查是否为空。

我担心它只会引起错误的自信感,最终会捕捉到琐事,而真正的 null 来源却未被发现。

您认为不可为空类型会错过哪些非琐事?

这并不完全正确。 即使服务器是用 TypeScipt 编写的,如果不检查从持久存储中获得的每个对象的每个属性,就无法保证引入空值。

如果持久存储支持类型化数据,那么就没有必要了。 但即使情况并非如此,您也只能在数据获取点进行检查,然后在其他代码的整个_所有_ 中得到保证。

我有点同意@aleksey-bykov 的看法。 如果我们可以让 TypeScript 在编译时就 null 和 undefined 引入的错误向我们发出警报,那将是非常棒的,但我担心它只会引起错误的自信感,最终会捕捉到琐事,而真正的 null 来源却未被发现。

使用可为空类型并不是绝对的要求。 如果您觉得没有必要对方法返回 null 的情况进行建模,因为它们“无关紧要”,您就不能在该类型定义中使用可为 null 的类型(并且会像往常一样获得相同的不安全性)。 但是没有理由认为这种方法会失败——有一些语言已经成功实现了它(例如JetBrains 的Kotlin

@aleksey-bykov 老实说,您完全错了,关于不可空类型的最好的事情之一是_可以将类型表示为可空_。
使用从不使用 null 来防止空指针错误的策略,您完全失去了使用null可能性,因为害怕引入错误,这完全是愚蠢的。

另一件事请在关于语言功能的讨论中不要使用愚蠢的评论,例如:

不可为空的类型是无用的。 不可为空的类型是有用的。 他们没用。 无用! 你没有意识到,但你并不真正需要它们。

那只是让我觉得我应该忽略您将在网络上的任何地方发布的任何内容,我们在这里讨论一个功能,我可以理解并欣然接受您的观点不是我的观点,但不要表现得像一个孩子。

我根本不反对引入非空类型注释。 它已被证明在 C# 和其他语言中很有用。

OP 通过以下方式改变了讨论的过程:

老实说,如果添加 use not-null 是避免所有空指针异常的代价,我会毫无问题地支付它,将 null 或 undefined 视为可分配给任何类型是我认为打字稿造成的更糟糕的错误。

我只是指出nullundefined在野外的流行。

我还应该补充一点,我真正欣赏 TypeScript 的一件事是该语言的自由放任态度。 这是一股清新的空气。

坚持认为类型在默认情况下不可为空是违背这种精神的。

我一直在看到许多关于为什么我们需要/不需要这个的争论,我想看看我是否理解到目前为止讨论过的所有潜在案例:

1) 想知道一个函数是否可以返回 null(由它的执行模式引起,而不是它正在输入)。
2)想知道一个值是否可以为空。
3) 想知道一个数据对象是否可以包含空值。

现在,只有两种情况会发生情况 2:使用空值或函数返回空值。 如果您从代码中消除所有空值(假设您不想要它们),那么实际上情况 2 只能作为情况 1 的结果发生。

情况 1 我认为最好通过注释函数的返回类型以显示空值的存在来解决。 _这并不意味着您需要非空类型_。 您可以注释函数(例如通过使用联合类型)并且没有非空类型,这就像文档一样,但在这种情况下可能更清楚。

方案2也由此解决。

这允许在他们公司工作的程序员使用流程和标准来强制标记空类型,而不是 Typescript 团队(完全相同的方式,整个打字系统是一个选择加入,所以显式可为空类型是一个选择在)。

至于场景 3,服务器和客户端之间的合同不是 Typescript 强制执行的,能够将受影响的值标记为可能为 null 可能是一种改进,但最终你会得到与 typescript 相同的 garentee工具为您提供了所有其他价值(也就是说,除非您有良好的编码标准或实践!

(电话发帖,有错误请见谅)

@fdecampredon ,首先不是恐惧,而是使用 null 是不必要的。 我不需要它们。 作为一个不错的奖励,我消除了空引用异常的问题。 这一切怎么可能? 通过使用带有空 case 的 sum-type。 Sum-types 是所有 FP 语言(如 F#、Scala、Haskell 以及称为代数数据类型的产品类型)的本机功能。 带有空大小写的 sum 类型的标准示例是来自 F# 的 Optional 和来自 Haskell 的 Maybe 。 TypeScript 没有 ADT,但它正在进行有关添加不可为 null 的讨论,以模拟 ADT 将涵盖的一种特殊情况。 所以我的信息是,抛弃 ADT 的不可为空。

@spion ,坏消息:F# 得到了空值(作为 .NET 的遗产)。 好消息:没有人使用它们。 当它在那里时,你怎么能不使用 null? 他们有 Optionals (就像你提到的最新的 Java)。 因此,如果您有更好的选择,则不需要 null。 这就是我最终的建议:不理会空值(非空值),只是忘记它们的存在,并将 ADT 实现为一种语言功能。

我们使用 null 但与人们使用的方式不同。 我公司的源代码分为两部分。

1)当涉及到数据(如数据库数据)时,我们在变量声明时将null替换为空白数据。

2) 所有其他的,比如可编程对象,我们使用 null 以便我们知道源代码中何时存在错误和漏洞,其中没有创建或分配不是必需的 javascript 对象的对象。 未定义是源代码中存在错误或漏洞的 javascript 对象问题。

我们不希望数据可以为空,因为它是客户数据,他们会看到空字。

@aleksey-bykov 打字稿具有联合类型的

另一方面,不可能定义联合类型排除空值,这就是我们需要非空类型的原因。

@fdecampredon ,TS 没有 ADT,它具有不是和类型的联合,因为,正如您所说,1. 由于没有单位类型,它们无法正确建模空案例,2. 没有可靠的解构方法他们

ADT 的模式匹配可以以与生成的 JavaScript 紧密对齐的方式实现,无论如何我希望这个论点不是一个转折点

@aleksey-bykov 这不是 F#。 它是一种旨在模拟 JavaScript 类型的语言。 JavaScript 库使用 null 和 undefined 值。 因此,应使用适当的类型对这些值进行建模。 由于甚至 ES6 都不支持代数数据类型,考虑到TypeScript 的设计目标,使用该解决方案是没有意义

此外,JavaScript 程序员通常使用 if 检查(与 typeof 和相等性测试结合使用),而不是模式匹配。 这些已经可以缩小 TypeScript 联合类型的范围。 从这一点来看,它只是支持不可为空类型的一小步,其好处可与代数 Maybe 等相媲美。

我很惊讶实际上没有人提到lib.d.ts可能需要引入的巨大变化以及在构造函数调用期间类字段的瞬态空状态的潜在问题。 这些是实现不可为空类型的一些真实的、实际的潜在问题......

@Griffork的想法是避免在您的代码中到处进行空检查。 假设你有以下功能

declare function getName(personId:number):string|null;

这个想法是您只检查名称是否为 null一次,然后执行所有其余代码而不必担心必须添加 null 检查。

function doSomethingWithPersonsName(personId:number) {
  var name = getName(personId);
  if (name != null) return doThingsWith(name); // type guard narrows string|null to just string
  else { return handleNullCase(); }
}

现在你很棒! 类型系统保证doThingsWith将使用不为空的名称调用

function doThingsWith(name:string) {
  // Lets create some funny versions of the name
  return [uppercasedName(name), fullyLowercased(name), funnyCased(name)]
}

这些函数都不需要检查空值,代码仍然可以工作而不抛出。 而且,一旦您尝试将一个可为空的字符串传递给这些函数之一,类型系统就会立即告诉您发生了错误:

function justUppercased(personId:number) {
  var name = getName(personId);
  return uppercasedName(name); // error, passing nullable to a function that doesn't check for nulls.
}

这是一个巨大的好处:现在类型系统跟踪函数是否可以处理可空值,此外,它跟踪它们是否真的需要。 更干净的代码,更少的检查,更安全。 这不仅仅是关于字符串 - 使用类似runtime-type-checks的库,您还可以为更复杂的数据构建类型保护

如果您不喜欢跟踪,因为您觉得不值得对空值的可能性进行建模,您可以恢复到旧的不安全行为:

declare function getName(personId:number):string;

在这些情况下,打字稿只会在你做了明显错误的事情时警告你

uppercasedName(null);

坦率地说,我没有看到缺点,除了向后兼容性。

@fdecampredon联合类型就是这样,联合。 它们不是 _disjoint_ 联合又名总和。 见#186。

@aleksey-bykov

请注意,添加选项类型仍然是一个重大更改。

// lib.d.ts
interface Document {
    getElementById(id: string): Maybe<Element>;
}

...

// Code that worked with 1.3
var myCanvas = <HTMLCanvasElement>document.getElementById("myCanvas");
// ... now throws the error that Maybe<Element> can't be cast to an <HTMLCanvasElement>

毕竟,您现在可以通过解构获得穷人的选择类型

class Option<T> {
    hasValue: boolean;
    value: T;
}

var { hasValue, myCanvas: value } = <Option<HTMLCanvasElement>> $("myCanvas");
if (!hasValue) {
    throw new Error("Canvas not found");
}
// Use myCanvas here

但唯一的价值是如果 lib.d.ts(和任何其他 .d.ts,以及你的整个代码库,但我们假设我们可以修复它)也使用它,否则你会回到不知道是否除非您查看其代码,否则不使用 Option 的函数可以返回 null 或不返回。

请注意,我也不赞成默认情况下非空的类型(无论如何不适用于 TS 1.x)。 这是一个太大的突破性变化。

但是假设我们在谈论 2.0。 如果我们无论如何都要进行重大更改(添加选项类型),为什么不在默认情况下也使类型不可为空? 默认情况下使类型不可为空并添加选项类型不是排他性的。 后者可以是独立的(例如,在您指出的 F# 中),但前者需要后者。

@Arnavion ,有一些误解,我没有说我们需要替换签名,所有现有签名都保持不变,这是为了新的开发,您可以使用 ADT 或其他任何您想要的东西。 所以没有重大变化。 默认情况下没有任何东西被设为非空。

如果 ATD 在这里,则由开发人员通过将 then 转换为可选项来包装空值可能泄漏到应用程序中的所有位置。 这可以是一个独立项目的想法。

@aleksey-bykov

我没有说我们需要更换签名,所有现有的签名保持不变

_I_ 说需要更换签名,我已经给出了原因:

但唯一的价值是如果 lib.d.ts(和任何其他 .d.ts,以及你的整个代码库,但我们假设我们可以修复它)也使用它,否则你会回到不知道是否除非您查看其代码,否则不使用 Option 的函数可以返回 null 或不返回。


默认情况下没有任何东西被设为非空。 曾经。

对于 TS 1.x,我同意,只是因为它的重大变化太大了。 对于 2.0,在默认签名(lib.d.ts 等)中使用选项类型已经是一个重大变化。 默认情况下使类型不可为空_除此之外_变得值得并且没有缺点。

我不同意,引入可选项不应该破坏任何东西,这不像我们要么使用可选项、可空值或非可空值。 每个人都使用他们想要的任何东西。 旧的做事方式不应该依赖于新功能。 开发人员可以根据自己的直接需求使用合适的工具。

所以你是说,如果我有一个返回 Option<number> 的函数 foo 和一个返回数字的函数 bar,除非我查看 bar 的实现或维护文档,否则我不能确信 bar 不能返回 null “这个函数永远不会返回空值。”? 您不认为这会惩罚永远不会返回 null 的函数吗?

您的示例中的函数bar从时间开始就被称为可为空的,它在周围的一些 100500 应用程序中使用,每个人都将结果视为可为空的,现在您转过来查看它的内部并发现 null是不可能的,这是否意味着您应该继续将签名从可为空更改为不可为空? 我认为你不应该。 因为这些知识虽然有价值,但不值得制动 100500 次应用程序。 您应该做的是想出一个带有修改签名的新库,如下所示:

old_lib.d.ts

...
declare function bar(): number; // looks like can return a potentially nullable number
...

修订版_lib.d.ts

declare function bar(): !number; // now thank to the knowledge we are 100% certain it cannot return null

现在遗留应用程序继续使用old_lib.d.ts ,对于新应用程序,开发人员可以自由选择revised_libs.d.ts

不幸的是,revised_libs.d.ts 有 50 个其他函数,我还没有看过,所有这些函数都返回数字(但我不知道它是真的数字还是可空数字)。 现在怎么办?

好吧,慢慢来,寻求帮助,使用版本控制(根据您到目前为止所获得的知识水平,您可能希望以不断增加的版本号逐步发布它: revised_lib.v-0.1.12.d.ts

其实没必要。 在签名中返回可空类型但在实现中返回不可为空类型的函数只会导致调用者进行冗余错误检查。 它不会危及安全。 逐渐用 ! 正如您所说,当您发现它们会正常工作时。

我不是粉丝! 只是因为打字更麻烦(在击键方面和需要记住使用它)。 如果我们想要 1.x 中的不可空性,那么 ! 是上面已经讨论过的选项之一,但我仍然会说,最终对 2.0 进行重大更改并将不可空性设为默认值是值得的。

另一方面,也许它会导致 Python 2/3 式的情况,多年来没有人升级到 TS 2.0,因为他们无法通过他们的百万行代码库确保每个变量声明和类成员和函数参数和...用 ? 如果它可以为空。 即使是 2to3(Python 2 到 3 迁移工具)也不必处理这样的广泛变化。

2.0 能否成为一个突破性的改变取决于 TS 团队。 我会投票赞成,但是我没有需要修复的百万行代码库,所以也许我的投票不算数。

也许我们应该问问 Funscript 的人,他们如何协调返回空值的 DOM API 与 F#(Funscript 使用 TypeScript 的 lib.d.ts 和其他 .d.ts 用于 F# 代码)。 我从来没有使用过它,但是例如查看http://funscript.info/samples/canvas/index.html似乎类型提供程序不认为 document.getElementsByTagName("canvas")[0] 可以不明确的。

编辑:这里似乎 document.getElementById() 不会返回 null。 至少它似乎没有返回 Option<Element> 看到它正在访问结果上的 .onlick 。

@spion
谢谢,我没想到。

在工作中,我们的代码库并不小,人们想要的这种突破性的改变会让我们耽误很多时间,却收获甚微。 通过良好的标准和我们的开发人员之间的清晰沟通,我们没有遇到不应该出现空值的问题。
老实说,我很惊讶有些人如此强烈地推动它。
不用说,这不会给我们带来什么好处,而且会花费我们很多时间。

@Griffork在打字稿编译器中查看了这个

关于破损,我认为如果您继续使用现有的类型定义,则可能根本不会出现任何错误,除非编译器指出可能未初始化的变量和字段泄漏。 另一方面,您可能会遇到很多错误,特别是例如如果类字段经常在代码的构造函数中未初始化(稍后初始化)。 因此,我理解您的担忧,并且我不会推动对 TS 1.x 进行向后不兼容的更改。 我仍然希望我已经设法说服你,如果对语言的任何更改值得破坏向后兼容性,那就是这个。

无论如何, Facebook 的 Flow确实具有不可为空的类型。 一旦它更加成熟,对于我们这些关心这个问题的人来说,作为 TS 的替代品可能值得研究。

@spion ,您的列表给我们的唯一数字是出于任何原因提及 null 或 undefined 的次数,基本上它只说已讨论过 null 和 undefined ,我希望很清楚您不能将其用作参数

@aleksey-bykov 我不是 - 我查看了该过滤列表中的_所有_问题,并且其中包含“崩溃”一词的每个问题都与堆栈跟踪有关,该堆栈跟踪表明函数试图访问属性或方法未定义或空值。

我试图用不同的关键字缩小过滤器的范围,我(认为我)设法获得了所有关键字

@spion
问题:这些错误中有多少是在他们需要将变量标记为可空或不可定义的位置引起的?

EG 如果一个对象可以有一个父对象,并且您将父对象初始化为 null,并且您总是会拥有一个父对象为 null 的对象,那么您仍然必须将父对象声明为可能为 null。
这里的问题是,如果程序员编写一些代码时假设循环在到达空父级之前总是会中断。 这不是语言中的 null 问题, undefined 会发生完全相同的事情

保持 null 像现在一样易于使用的原因:
• 这是比未定义更好的默认值。
. 1)在某些情况下检查速度更快(我们的代码必须非常高效)
. 2) 它使循环在对象上的工作更可预测。
. 3) 当对空值使用空值(而不是缺失值)时,它使数组使用更有意义。 请注意, delete array[i]array[i] = undefined在使用 indexOf(可能还有其他流行的数组方法)时有不同的行为。

我觉得制作空值的结果需要额外的标记才能在语言中使用:
• 我得到了一个未定义的错误而不是一个空错误(这是大多数打字稿场景中会发生的情况)。

当我说我们没有空转义问题时,我的意思是未初始化为空的变量永远不会变为空,我们仍然会在与未定义错误完全相同的地方遇到空异常错误(就像 Typescript 团队一样)。 使 null 更难使用(通过需要额外的语法)并保持 undefined 不变实际上会给某些开发人员(例如我们)带来更多问题。

添加额外的语法以使用 null 意味着在几周/几个月内,使用大量 null 的开发人员会在尝试记住新语法时向左、向右和居中出错。 这将是未来滑倒的另一种方式(通过稍微错误地注释一些东西)。 [是时候指出我讨厌使用符号来表示类型的想法,它使语言变得不那么清晰]

除非您能向我解释 null 导致 undefined 不会导致的错误或问题的情况,否则我不会同意让 null 比 undefined 更难使用。 它有它的用例,仅仅因为它没有帮助,并不意味着你想要的破坏性改变(会_会_伤害其他开发人员的工作流程)应该继续进行。

结论:
在不能定义非未定义类型的情况下声明不可为空的类型是没有意义的。 由于 javascript 的工作方式,非未定义类型是不可能的。

@Griffork当我说不可为空时,我的意思是不可为空或未定义。 由于 JS 的工作方式,它不可能实现,这是不正确的。 使用新的类型保护功能,一旦您使用类型保护,您就会知道从那里流出的值不能再为 null 或未定义。 那里也涉及权衡,我提交了Facebook Flow 的实现,作为它非常可行的证据。

_only_ 将变得稍微难以使用的情况是这样的:我将暂时将 null 分配给这个变量,然后我将在另一个方法中使用它,就好像它不为 null,但我知道我之前永远不会调用这个其他方法首先初始化变量,所以不需要检查空值。 这是非常脆弱的代码,我绝对欢迎编译器警告我:无论如何,它是一个重构而不是一个错误。

@spion
我相信我终于明白你来自哪里。

我相信您需要类型保护来帮助确定值何时不能为空,并允许您调用不检查空值的函数。 如果删除了保护 if 语句,那么这将成为一个错误。

我可以看到这很有用。

我也相信这并不是你所希望的灵丹妙药。

一个不运行你的代码并测试它的每个方面的编译器在确定 undefineds/nulls 的位置方面不会比编写代码的程序员更好。 我担心这种变化会让人们产生一种错误的安全感,并且实际上使 null/undefined 错误在它们确实发生时更难以跟踪。
真的,我认为您需要的解决方案是一组支持 Typescript 的良好测试工具,可以在 Javascript 中重现此类错误,而不是在编译器中实现无法兑现承诺的类型。

您提到 Flow 可以解决此问题,但是在阅读您的链接时,我看到了一些相关内容:

“流程有时也可以调整对象属性的类型,特别是当检查和使用之间没有中间操作时。不过,一般来说,对象的别名限制了这种推理形式的范围,因为对对象属性的检查可能通过别名写入该属性而无效,并且静态分析很难精确跟踪别名。”

“未定义的值,就像 null 一样,也会导致问题。不幸的是,未定义的值在 JavaScript 中无处不在,很难在不严重影响语言可用性的情况下避免它们[...] Flow 在这种情况下进行权衡:它检测未定义的局部变量和返回值,但忽略了由对象属性和数组元素访问导致的未定义的可能性。”

现在 undefined 和 null 的工作方式不同,未定义的错误仍然会出现在任何地方,问号不能保证该值不为 null,并且语言的行为与 Javascript 更不同(这是 TS 试图从我所看到的情况中避免的) )。

ps

foo(thing: whatiknowaboutmyobject) {
    if (thing.hidden) {
        delete thing.description;
    }
}

if (typeof thing.description === "string") {
    //thing.description is non-nullable now, right?
    foo(thing);
    //What is thing.description?
    console.log(thing.description.length);
}

TS 已经很容易受到混叠效应的影响(就像任何允许可变值的语言一样)。 这将编译没有任何错误:

function foo(obj: { bar: string|number }) {
    obj.bar = 5;
}

var baz: { bar: string } = { bar: "5" };

foo(baz);

console.log(baz.bar.charAt(0)); // Runtime error - Number doesn't have a charAt method

Flow 文档只是为了完整性而说明这一点。

编辑:更好的例子。

老的:

是的,但我认为将某物设置为 undefined 的方法远大于将某物设置为另一个值的方法。

我是说,

mything.mystring = 5; // is clearly wrong.
delete mything.mystring; //is not clearly wrong - this is not quite the equivalent of setting mystring to >undefined.

编辑:
嗯,在这一点上,这几乎是个人喜好。 在使用 javascript 多年之后,我认为这个建议不会对语言有所帮助。 我认为它会让人们产生一种错误的安全感,而且我认为它会驱使 Typescript(作为一种语言)远离 Javascript。

@Griffork有关当前打字稿如何让您产生虚假安全感的示例,请尝试您在操场上展示的示例:

var mything = {mystring: "5"}; 
delete mything.mystring;
console.log(mything.mystring.charAt(1));

顺便说一下,删除操作符的处理方式与分配 null 类型的值相同,这也足以涵盖您的情况。

声称该语言的行为将与 JavaScript 不同的说法是正确的,但毫无意义。 TypeScript 已经具有与 JavaScript 不同的行为。 类型系统的重点一直是禁止没有意义的程序。 对不可空类型建模只是增加了一些额外的限制。 禁止将空值或未定义值分配给不可为空类型的变量与禁止将数字分配给字符串类型的变量完全相同。 JS 两者都允许,TS 两者都不允许

@spion

这个想法是为了避免在你的代码中到处都有空检查

如果我理解你的主张:

A. 默认情况下将所有类型设为非空。
B. 标记可以为空的字段和变量。
C. 确保应用程序/库开发人员检查应用程序的所有入口点。

但这是否意味着确保代码没有空值的责任在于编写代码的人,而不是编译器? 我们有效地告诉编译器“dw,我不会让任何空值进入系统。”

另一种方法是说,空值无处不在,所以不要打扰,但如果某些内容不可为空,那么我会告诉你。

事实是后一种方法容易出现空引用异常,但它更真实。 假装通过线路(即 ajax)获得的对象上的字段非空意味着对上帝有信心 :smiley: 。

我相信在这个问题上存在很大的分歧,因为根据人们正在处理的内容,上面的C可能是微不足道的,也可能是不可行的。

@jbondc我很高兴你这么问。 实际上,CallExpression 被标记为未定义或可为空。 然而,类型系统目前没有利用这一点——它仍然允许对typeArguments所有操作,就好像它不是空的或未定义的一样。

但是,当将新联合类型与不可为 null 的类型结合使用时,该类型可以表示为NodeArray<TypeNode>|null 。 然后,除非应用空检查,否则类型系统将不允许对该字段进行任何操作:

if (ce.typeArguments != null) {
  callSomethingOn(ce.typeArguments)
}

// callSomethingOn doesn't need to perform any checks

function callSomethingOn(na:NodeArray<TypeNode>) {
...
}

在 TS 1.4 类型保护的帮助下,在 if 块中,表达式的类型将缩小为NodeArray<TypeNode> ,这反过来将允许对该类型的所有NodeArray操作; 此外,该检查中调用的所有函数都可以将其参数类型指定为NodeArray<TypeNode>而无需执行任何更多检查。

但是如果你试着写

function someOtherFunction(ce: CallExpression) {
  callSomethingOn(ce.typeArguments)
}

编译器会在编译时警告你,这个错误根本不会发生。

所以不, @NoelAbrahams ,这并不是要确定一切。 它是关于编译器帮助您判断变量或字段可以包含什么值,就像所有其他类型一样。

当然,对于外部值,一如既往,由您来指定它们的类型。 你总是可以说外部数据包含一个字符串而不是一个数字,编译器不会抱怨你的程序在尝试对数字进行字符串操作时会崩溃。

但是如果没有不可为空的类型,您甚至无法说值不能为空。 空值可分配给每种类型,您不能对其进行任何限制。 变量可以保持未初始化状态,您不会收到任何警告,因为undefined是任何类型的有效值。 因此,编译器无法帮助您在编译时捕获与空值和未定义相关的错误。

我发现对不可为空类型有如此多的误解令人惊讶。 它们只是不能保持未初始化或不能分配值nullundefined 。 这与无法将字符串分配给数字没有什么不同。 这是我关于这个问题的最后一篇文章,因为我觉得我并没有真正取得任何进展。 如果有人有兴趣了解更多信息,我建议从我上面提到的视频“十亿美元的错误”开始。 这个问题是众所周知的,许多现代语言和编译器都成功地解决了这个问题。

@spion ,我完全同意能够声明类型是否可以为空的所有好处。 但问题是你希望类型是非空的_by default _?

假装通过线路(即 ajax)获得的对象上的字段是非空的意味着对上帝有信心

所以不要假装。 将其标记为可空,您将被迫在将其用作不可空之前对其进行测试。

当然。 归结为我们是要将字段标记为可空(在字段默认为非空的系统中)还是我们是否要将字段标记为不可为空(在字段默认为可空的系统中)。

争论是前者是一个重大变化(可能重要也可能不重要)并且也是站不住脚的,因为它需要应用程序开发人员检查并保证所有入口点。

@NoelAbrahams我不明白为什么它基本上大部分时间你不想要空值,当入口点可以返回空值时,你将不得不检查它,最终是一个具有非空类型的类型系统默认情况下将允许您进行较少的空检查,因为您将能够信任应用程序中的某些 api/library/入口点。

当您考虑一下在可空类型系统中将类型标记为非空具有有限的价值时,您仍然可以使用可为空类型的变量/返回类型而无需被迫对其进行测试。
它还将迫使定义作者编写更多代码,因为大多数时候设计良好的库从不返回 null 或未定义的值。
最后,甚至这个概念也很奇怪,在具有不可空类型的类型系统中,可空类型完全可以表示为联合类型: ?string相当于string | null | undefined 。 在默认类型为可空类型的类型系统中,您可以将类型标记为不可为空,您将如何表达!stringstring - null - undefined ?

最后我不是很明白这里的人的顾虑, null不是字符串,就像5不是字符串一样,两个值都不能用于需要字符串的地方,并且让滑移var myString: string = null容易出错: var myString: string = 5
将 null 或 undefined 赋值给任何类型可能是开发人员熟悉的概念,但它仍然是一个糟糕的概念。

我认为我在之前的帖子中并不完全正确:我会将其归咎于时间。

我刚刚浏览了我们的一些代码,看看事情是如何工作的,将某些字段标记为可空肯定会有所帮助,例如:

interface Foo {
        name: string;
        address: string|null; /* Nullable */
}

var foo:Foo = new FooClass();
foo.name.toString(); // Okay
foo.address.toString(); // Error: do not use without null check

但我反对的是以下几点:

foo.name = undefined; // Error: non-nullable

我觉得这会干扰使用 JavaScript 的自然方式。

完全相同的可能适用于 number :

interface Foo {
        name: string;
        address: string|number; 
}
var foo:Foo = new FooClass();
foo.name.toString(); // Okay
foo.address.slice() // error

foo.name  = 5 // error

它在 JavaScript 中仍然有效

合理地,您愿意将 null 分配给对象的属性多少次?

我认为大多数事情都会被标记为空,但你会依赖类型保护来声明该字段现在不可为空。

@fdecampredon
其实挺多的。

@格里福克

我认为大多数事情都会被标记为 null

这是我最初的想法。 但是在浏览了我们代码的某些部分后,我发现了如下注释:

interface MyType {

     name: string;

     /** The date the entry was updated from Wikipedia or undefined for user-submitted content. */
     wikiDate: Date; /* Nullable */
}

字段可为空的想法通常用于提供信息。 如果在访问wikiDate时需要类型保护,TypeScript 将捕获错误。

@fdecampredon

foo.name = 5 // error
它在 JavaScript 中仍然有效

没错,但这是一个错误,因为 TypeScript 100% 肯定地知道这不是故意的。

然而

foo.name = 未定义; // 不向服务器发送名称

完全是故意的。

我认为最符合我们要求的实现是不使用联合类型,而是遵循最初的建议:

 wikiDate: ?Date;

我同意@NoelAbrahams

foo.name = 5 // 错误
它在 JavaScript 中仍然有效
没错,但这是一个错误,因为 TypeScript 100% 肯定地知道这不是故意的。

编译器只知道您将 name 标记为string而不是string | number如果您想要一个可为空的值,您只需将其标记为?stringstring | null (几乎是等价的)

我认为最符合我们要求的实现是不使用联合类型,而是遵循最初的建议:

维基日期:?日期;

所以我们同意默认情况下类型是非空的,你可以用? :) 标记为可空。
请注意,这将是一个联合类型,因为?Date相当于Date | null | undefined :)

哦对不起,我试图同意默认情况下可以为空,而不是特殊类型的空(符号令人困惑)。

@fdecampredon ,实际上它的意思是当一个字段或变量被标记为可空时,那么访问需要一个类型保护:

var wikiDate: ?Date;

wikiDate.toString(); // error
wikiDate && wikiDate.toString(); // okay

这不是一个重大变化,因为我们仍然应该能够做到这一点:

 var name: string;   // okay
 name.toString();  // if you think that's fine then by all means

也许您认为如果不将null引入联合类型,我们就不能拥有它?

当你这样做时,你的第一个例子是绝对正确的:

wikiDate && wikiDate.toString(); // okay

您使用类型保护,编译器不应该发出任何警告。

但是你的第二个例子不好

var name: string;   // okay
name.toString();  // if you think that's fine then by all means

编译器在这里应该有一个错误,一个简单的算法可能只是在第一行出错(未标记为可为空的未初始化变量),一个更复杂的算法可以尝试在第一次使用之前检测分配:

var name: string;   // okay
name.toString();  // error because not initialized
var name: string;
if (something) {
  name = "Hello World";
} else {
  name = "Foo bar";
}
name.toString();  // no error since name will always be initialized.

我不知道究竟把屏障放在哪里,但它肯定需要某种微妙的调整才能不妨碍开发者。

这是一个突破性的变化,不能在 2.0 之前引入,除非@metaweta提出的“

为什么没有:

var string1: string; //this works like typescript does currently, doesn't need type-guarding before use, null and undefined can be assigned to it.
string1.length; //works
var string2: !string; //this doesn't work because the string must be assigned to a non-null and non-undefined value, doesn't need type-guarding before use.
var string3: ?string; //this must be type guarded to non-null, non-undefined before use.
var string4: !string|null = null; //This may be null, but should never be undefined (and must be type-guarded before use).
var string5: !string|undefined; //this may never be null, but can be undefined (and must be type-guarded before use).

并且有一个编译器标志(只有在 -noimplicitany 开启时才有效),上面写着 -noinferrednulls,它禁用了类型(如 string 和 int)的正常语法,你必须提供一个 ? 或者 ! 与他们(空,未定义和任何类型是例外)。

通过这种方式,不可为空的项目是一种选择,您可以使用编译器标志来强制项目显式为空。
编译器在类型分配时标记错误,而不是之后(就像之前的提议一样)。

想法?

编辑:我写这个是因为它强制使用非空的想法在每个动作中都是明确的。 任何阅读来自任何其他 TS 项目的代码的人都会确切地知道发生了什么。 如果打开,编译器标志也变得非常明显(因为blah: string是一个错误,但blah:!string不是,类似于 -noimplicitany 的工作方式)。

编辑2:
然后可以升级 DefinatelyTyped 以支持 noninferrednulls,如果人们选择不选择加入 ? 和 ! 特征。

我不在乎 non-null 和 non-undefined 是否选择加入,选择退出,与
类型修饰符 (!?)、指令或编译器标志; 我会做任何事情
需要得到它们_只要它们可以表达_,这不是
目前的情况。

2014 年 12 月 22 日星期一下午 2:35,Griffork [email protected]写道:

为什么没有:

var string1:字符串; //这就像打字稿目前所做的一样。在使用前不需要类型保护,可以为其分配null和undefined。
string1.length; //worksvar string2: !string; //这不起作用,因为字符串必须分配给非空和非未定义的值,在使用之前不需要类型保护。var string3: ?string; // this 必须在使用前被类型保护为非空、非未定义。var string4: !string|null = null; //这可能是空的,但不应该是未定义的(并且必须在使用前进行类型保护)。var string5: !string|undefined; //this 可能永远不会为空,但可以是未定义的(并且必须在使用前进行类型保护)。

并有一个编译器标志(只有在 -noimplicitany 开启时才有效)
说 -noinferrednulls,它禁用了类型的正常语法(如
string 和 int) 并且您必须提供 ? 或者 ! 与他们(空,未定义
和任何例外)。

以这种方式,不可为空是一种选择,您可以使用编译器
标志强制项目显式为空。
编译器在 _assignment of types_ 处标记错误,而不是之后(例如
之前的提议)。

想法?

直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -67899445
.

Mike Stay - [email protected]
http://www.cs.auckland.ac.nz/~mike
http://reperiendi.wordpress.com

@Griffork这将是一种选择,但在我看来这是一个糟糕的选择,我将解释原因:

  • 这将导致定义文件中的更多工作,因为现在我们必须检查和注释每个类型以获得正确的定义。
  • 我们最终会在代码的任何地方写上!string (有时还有?string ),这会降低代码的可读性。
  • !string在类型可以为空的系统中是一个奇怪的概念,你不能真正描述它的唯一方法是string minus null minus undefined ,相反?string很容易描述默认情况下类型为 null string | null | undefined的类型系统。
  • 我预见到要找到一种类型检查算法(编译器理解string | null需要类型保护但string不需要类型检查算法)会很头疼(和性能损失),您基本上引入了一个概念,其中某些联合类型应与其他联合类型区别对待。
  • 最后最糟糕的部分是我们完全失去了类型推断var myString = "hello" myString 应该是string?string还是!string ? 老实说,这是一个令人头疼的问题。

如果我们没有默认的非空类型,我在这里看到的最好的建议是@metaweta 提出的'use non-null'指令。
当然需要很好地指定它,但至少在我们所有的文件中只使用use non-null字符串,我们可以获得一个简单的可预测行为。

@fdecampredon

  1. 在定义文件中可能需要做更多的工作,但是_无论如何你都必须这样做_(以确保类型正确)并且这次编译器会提醒您尚未编辑的内容(如果使用 -noimplicitnull )。
  2. 我愿意接受其他注释建议。 老实说,我相信当前的类型系统有它的位置,不应该_replaced_。 我不认为重大的改变是值得的。 相反,我认为我们应该找到一种更好的方式来描述您所追求的东西。 (我真的,真的不喜欢用符号表示任何这些的想法,它们不直观。)
  3. 有什么难以描述的? 我在打字稿的其他地方看到过讨论,其中提出了此请求(针对某些类型)(没有标记)。 快速搜索,我找不到问题,我稍后会追捕更多。

  4. 如果你指的是我写的!string|null ,如果 null 被处理为 _like_ {} (但不能分配给它),这将在当前系统中工作。
    如果您谈论的是我的列表中没有的string|null ,那么我认为在这种情况下应该忽略 null。 Null 和 undefined 仅在联合中有意义,其中每个非空和非未定义都以 ! 并且 any 不是 any (这可能是编译器警告/错误)。
  5. 好问题,而且只有在您使用 -noimplicitnull 选项时才会出现,我认为最安全的选项是将其分配给最有可能导致早期错误(可能为空)的选项,但我得到了感觉有一个我没有想到的更好的主意。 我想知道其他人是否对如何解决这个问题有建议?

编辑:添加到第 2 点。
编辑:修正了第 4 点中的错字。

在定义文件中可能需要做更多的工作,但无论如何您都必须完成这项工作(以确保类型正确),这一次编译器会提醒您尚未编辑的内容(如果使用 -noimplicitnull )。

是与否,请检查您使用的库并查看实际返回 null 或未定义的数量。
这是一个非常罕见的情况,我们只能在这个问题中发现标准库很少发生,例如 Promise 库从不这样做。
我的观点是,在默认情况下类型不可为空的类型系统中,大多数现有定义文件已经有效

我愿意接受其他注释建议。 老实说,我相信当前的类型系统有它的位置,不应该被替换。 我不认为重大的改变是值得的。 相反,我认为我们应该找到一种更好的方式来描述您所追求的东西。 (我真的,真的不喜欢用符号表示任何这些的想法,它们不直观。)

我不认为有这样的方式,但我希望我是错的,因为在我看来它具有不可估量的价值。
对于重大变更部分,您为什么如此反对'use non-null'指令?
希望可空类型系统的开发人员根本不会受到影响(除非他们已经奇怪地在文件顶部添加了'use non-null'但老实说这有点......奇怪)
希望非空类型系统的开发人员可以使用新指令。

有什么难以描述的? 我在打字稿的其他地方看到过讨论,其中提出了此请求(针对某些类型)(没有标记)。 快速搜索,我找不到问题,我稍后会追捕更多。
我只是觉得这个概念有点“奇怪”而且不是很清楚,我通常使用组合作为编程的主要工具,而不是 _decomposition_ 但为什么不呢。

如果你指的是我写的 !string|null ,如果 null 被视为 {} (但不能分配给它),它会在当前系统中工作。 如果您正在谈论我的列表中没有的 string|null,那么我认为在这种情况下应该忽略 null。 Null 和 undefined 仅在联合中有意义,其中每个非空和非未定义都以 ! 并且 any 不是 any (这可能是编译器警告/错误)。
我指的是当你分解 3 种类型时:

  • !sting : string - null - undefined
  • string : string | null | undefined
  • ?string : string | null | undefined

最新的 2 个基本上没有区别,但是编译器应该知道对于string它不应该强制类型保护检查而对于?string它应该,该信息必须在任何地方传播,算法会比实际复杂得多,而且我很确定我可以通过类型推断找到奇怪的情况。

好问题,而且只有在您使用 -noimplicitnull 选项时才会出现,我认为最安全的选项是将其分配给最有可能导致早期错误(可能为空)的选项,但我得到了感觉有一个我没有想到的更好的主意。 我想知道其他人是否对如何解决这个问题有建议?

不会这样做基本上会引入与@RyanCavanaugh评论相同的问题,当我考虑只引入一个标志时,该标志将允许从默认情况下的 null 切换到默认情况下的 null。
在这种情况下,前一个命题要简单得多。

再说一遍,你为什么反对'use non-null'指令,我想得越多,在我看来它是理想的解决方案。

@fdecampredon
因为目前提出的“使用非空”改变了语言_使用_的方式,而不是语言_书面_的方式。 这意味着从一个位置移动到另一个位置的函数在_used_ 时可能会以不同的方式工作。 您将得到可能在 1-3 个文件之外的编译时错误,因为某些内容输入不正确。

和...之间的不同:
string

?string
那是? 和 ! 符号要求对这个变量进行严格的类型检查(很像你的“use nonnull”指令,但在每个变量的基础上)。 这样解释,我认为人们理解它不会有太多麻烦。

区别当然是:
文件 1:

//...187 lines of code down...
string myfoo(checker: boolean) {
    if(checker){
        return null;
    }
    else {
        return "hello";
    }
}

文件2:

"use nonnull"
//...2,748 lines of code down...
string myfoo(checker: boolean) {
    if(checker){
        return null; //Error!
    }
    else {
        return "hello";
    }
}

开发人员现在必须保留哪些文件是非空文件以及哪些文件不是的思维导图。 老实说,我认为这是一个主意(即使大部分 _your_ 代码是“使用非空”)。

编辑:同样,当你开始输入一个函数并且你得到一个告诉你函数定义是什么的小窗口时,你怎么知道该定义中的string是可空的还是不可空的?

@格里福克

  • 'use strict'也是这种情况。
  • 至少这个想法很简单。 并在类型系统中引入简单的概念。
  • 如果开发人员在另一个文件中移动一个函数对我来说似乎是一个非常边缘的情况,并且由于他将被提示的错误将是关于空检查的,他将很快能够理解发生了什么......

同样它并不完美,但我没有看到更好的选择。

需要明确的是,我所提出的(在反驳之后)我能看到的唯一问题是:

  • 我们不知道var mystring = "string"的行为是什么
  • 您不想在任何地方都键入符号。

我对指令的担忧是:

  • 非空值不会是明确的(但可以与可空值出现在同一个项目中)。 编辑:固定措辞更有意义。
  • 开发人员将更难跟踪他们正在编写的内容是可为空的还是不可为空的。
  • 您在调用函数时看到的函数定义(编辑:Visual Studio 在您开始键入函数时提供的弹出窗口)可能是也可能不是可空的,您将无法分辨。
  • 一旦一个函数被包装了 2 层或 3 层,你就不知道它的定义是否仍然正确(因为你不能通过没有“use nonnull”的文件使用类型推断)。

老实说,“严格使用”的实施并不是我们应该渴望的。 它是为 JavaScript(不是 Typescript)设计的,在 JavaScript 中几乎没有替代品。 我们正在使用 Typescript,因此我们可以选择做得更好。
而且我不明白为什么指令比强迫开发人员明确他们的意图更好(毕竟,这就是创建 Typescript 的原因,不是吗?)。

编辑:澄清了一点。
编辑:将字符串放在引号中

老实说,你对我的关注的总结有点小,并没有反映我写的内容,你的提议增加了类型系统的复杂性,使类型检查算法令人头疼,很难用所有的边缘情况来指定它会专门为类型推断而创建,并使代码变得超级冗长。 基本上不是适合这项工作的工具。

我希望所有非空值都是显式的(因为它们可以与可空值出现在同一个项目中)。

我想要string实际上是string | null | undefined的事实,而不必强迫您检查这一点,明确的。

开发人员将更难跟踪他们正在编写的内容是可为空的还是不可为空的。

如果项目使用非空,我怀疑项目中是否会有一个没有“使用非空”的文件,并且在文件顶部滚动并不是那么难(至少当您编写的文件少于 500 行代码时)这是大多数情况......)

您在调用函数时看到的函数定义可能可以也可能不可以为空,并且您无法分辨。

是的,如果它来自具有指令“使用非空”的模块,那么您会相应地输入内容,如果您不只是将所有内容都视为可以为空...

一旦一个函数被包装了 2 层或 3 层,你就不知道它的定义是否仍然正确(因为你不能通过没有“use nonnull”的文件使用类型推断)。

我不明白你在这里的意思。

老实说,“严格使用”的实施并不是我们应该追求的。 它是为 JavaScript(不是 Typescript)设计的,在 JavaScript 中几乎没有替代品。 我们正在使用 Typescript,因此我们可以选择做得更好。
而且我不明白为什么指令比强迫开发人员明确他们的意图更好(毕竟,这就是创建 Typescript 的原因,不是吗?)。

TypeScript 是 javascript 的超集,它起源于 JavaScript,其目标是让您以更安全的方式编写 javascript,因此重用 JavaScript 的概念似乎并非没有道理。

我不明白为什么指令比强迫开发人员明确他们的意图更好

因为您将无法获得相同的结果,出于我列举的所有原因,在可空类型系统中使用不可空类型只是一个坏主意。

从我的角度来看,这里有 3 个可行的解决方案:

  • 类型变为不可为空的 2.0 的重大更改
  • 默认情况下切换到不可为空类型的编译器标志,就像我提出的几十条评论一样,但@RyanCavanaugh 对此有一个很好的观点。 (如果我真的认为值得的话)
  • “使用非空”指令

顺便说一下,这面旗帜对我来说是完全值得的。

function fn(x: string): number;
function fn(x: number|null): string;

function foo() {
    return fn(null);
}

var x = foo(); // x: number or x: string?

如果函数是唯一的问题,则在重载包含显式null参数的情况下可能会出现异常 - 它始终优先于隐式 null(为了与--noImplicitNull和谐) . 这也是对用户有意义的解释(即“我明确说的应该覆盖隐含说的内容”)。 尽管我确实想知道该标志是否还有其他类似的问题无法通过这种方式解决。 当然,这给规范和实现都增加了一些黑客的复杂性:|

  1. string|null|undefined在我的提案中使用标志是明确的,这就是我忽略它的原因。
  2. 为什么每个文件都有它呢? 我提出我的建议是因为它_不破坏向后兼容性_,这对我很重要,可能还有很多其他开发人员。 它可以被强制为项目范围的。
  3. 我使用了很多我没有创建的文件; 我怎么知道那个特定文件有“使用非空”? 如果我有 100 个其他人制作的文件,我必须记住这 100 个文件中的哪个是非空的? (或者,_每次_我使用另一个文件中的变量/函数,我必须打开该文件并检查它?)
  4. 让我们看看我是否可以更清楚地说明这一点:文章末尾的示例。
  5. 你停在哪里? 你说哪里不好? 文件开头的字符串或关键字不应改变文件的行为方式。 “使用严格”被添加到 javascript 中,因为

    1. 当时不存在可以做它想做的事情的大型超集 Javascript(例如 Typescript)。

    2. 这是为了加快JavaScript

      采用“严格使用”_不是因为这是正确的做法_而是因为这是浏览器能够满足开发人员需求的唯一方式。 我不想看到打字稿添加 1(然后是 2,然后是 3 然后是 4)其他指令,这些指令_从根本上改变了语言的工作方式_作为在某个任意范围内声明的字符串,并影响其他一些任意范围。 这真是糟糕的语言设计。 如果 Typescript 中不存在“use strict”,而是一个编译器标志(并且 Typescript 将它发送到需要它的每个文件/范围中),我会很高兴。

  6. Javascript 不是“不可为空的类型系统”,Typescript 也不是“不可为空的类型系统”。 引入声明不可为空类型的能力并不意味着整个系统是“不可为空的”。 这不是 Typescript 的重点。
File 1:
"use notnull"
export string foo() {
    return "mygeneratedstring";
}
File 2:
export string foo() {
    return file1.foo()
}
File 3:
"use notnull"
file2.foo(); //???

您实际上可以在仍然使用相同语法的情况下丢失上下文信息! 这不是我期待在我使用的任何语言中拥有的东西。


我们似乎在绕圈子争论。 如果我对你没有意义,那么我很抱歉,但你似乎反复提出同样的问题,我反复回复它们。

@spion
在该示例中,字符串不是隐式空值(我相信它假定 --noImplicitNull 标志已打开)

string|null|undefined 在我的提案中使用标志是明确的,这就是为什么我把它留给了

我的意思是,在实际系统中var myString: string是隐式的var myString: (string | null | undefined);并且除此之外编译器不会强制您使用 typeguard,除非所有其他联合类型。

那为什么每个文件都有它呢? 我提出我的建议,因为它不会破坏对我很重要的向后兼容性,可能还有很多其他开发人员。 它可以被强制为项目范围的。
指令不会破坏向后兼容性(除非您出于非常奇怪的原因在指令的位置使用字符串文字“use not-null”,我敢打赌没有人这样做),而且在某些时候TypeScript 将不得不破坏向后兼容性,这就是 semver 定义主要版本的原因,但那是另一回事了。

我使用了很多我没有创建的文件; 我怎么知道那个特定文件有“使用非空”? 如果我有 100 个其他人制作的文件,我必须记住这 100 个文件中的哪个是非空的? (或者,每次我使用另一个文件中的变量/函数时,我都必须打开该文件并检查它?)

如果你在一个有指导方针等的项目中,就像每个项目都是正常组织的一样,你知道......在最坏的情况下你只需要滚动一点......看起来并不难......

你停在哪里? 你说哪里不好? 文件开头的字符串或关键字不应改变文件的行为方式。 “使用严格”被添加到 javascript 中,因为
当时不存在可以做它想做的事情的大型超集 Javascript(例如 Typescript)。
这是为了加快 JavaScript 的处理速度(在我看来这是唯一可以原谅的原因)。 采用“严格使用”并不是因为它是正确的做法,而是因为它是浏览器能够满足开发人员需求的唯一方式。 我不想看到打字稿添加 1(然后是 2,然后是 3,然后是 4)其他指令,这些指令从根本上改变了语言作为在某个任意范围内声明的字符串的工作方式,并影响其他一些任意范围。 这真是糟糕的语言设计。 如果 Typescript 中不存在“use strict”,而是一个编译器标志(并且 Typescript 将它发送到需要它的每个文件/范围中),我会很高兴。

引入“use strict”是为了改变语言行为并保持复古兼容性,这与我们现在面临的问题完全相同,并且或多或少会随着 es6 模块而消失(因此 use not null 可能会随着 typescript 的下一个主要版本而消失) .

Javascript 不是“不可为空的类型系统”,Typescript 也不是“不可为空的类型系统”。 引入声明不可为空类型的能力并不意味着整个系统是“不可为空的”。 这不是 Typescript 的重点。

这句话没有任何意义对我来说,JavaScript没有静态类型系统,所以像我说的是事实,你可以分配null来那是以前的变量string可以很容易地比较事实上,您可以将5分配给之前是字符串的变量。
唯一的区别是 TypeScript,一个为 JavaScript 创建的类型检查器,自以为是地认为null可以分配给所有东西,而不是5

对于您的示例,是的,我们在文件之间丢失了一些信息,这对我来说是一个可以接受的权衡,但我可以理解您的担忧。

我们似乎在绕圈子争论。 如果我对你没有意义,那么我很抱歉,但你似乎反复提出同样的问题,我反复回复它们。

是的,我同意,我怀疑是否会就此达成任何共识,老实说,我认为我最终会分叉编译器并为自己添加非空类型,希望有一天它会在主编译器或流程中结束成熟到可以使用。

@Griffork顺便说一句,我认为@spion 的想法确实有效,这个想法是说如果没有标志string是一个隐式可空的number | null总是会在第一次重载时流行,所以在两者中情况foo(null)将是string

@RyanCavanaugh你认为它可以工作吗?

@fdecampredon

  1. 引入了“use strict”来优化 javascript。 它必须被浏览器使用,因此它必须在代码中。 而且它必须向后兼容,所以它必须是一个非功能性的语句。
    浏览器不必使用不可为 null 的类型,因此它不必具有代码内指令。
  2. 抱歉,我指的是我忘记发布的内容,这里是:

根据Facebook 的说法:“在 JavaScript 中,null 隐式转换为所有原始类型;它也是任何对象类型的有效居民。”
我希望 non-nullable 是显式的(在开发人员所做的每个操作中)的原因是因为通过使用 non-nullable,开发人员承认他们与 javascript 的工作方式不同,并且类型是不可为 null 的不保证(因为有很多事情会导致这出错)。
不可为空的属性是永远无法删除的属性。

我已经使用 Typescript 很长时间了。 我最初很难说服我的老板从 Javascript 更改为 Typescript,而且我很难说服他在发生重大更改时让我们升级(即使没有也不容易)。 当浏览器支持 ES:Harmony 时,我们可能希望使用它。 如果从现在到 Typescript 支持 ES:Harmony(特别是像非 nullable 那样普遍)之间在 Typescript 中发生重大变化,我怀疑我会赢得那个论点。 更不用说我重新培训所有开发人员需要花费的时间和金钱(我是我们的“Typescript 人”,我的工作是培训我们的 Typescript 开发人员)。
因此,我真的反对引入重大更改。
如果有一种方法可以在不破坏旧代码的情况下将它们引入未来的代码,我不介意自己能够使用不可空值(这就是我提议的原因)。 理想情况下,最终我们所有的代码都会被转换,但它会大大减少我们在培训人员和使用 3rd 方库方面花费的时间和金钱。

我喜欢使用符号(根据我的建议)而不是每个文件指令的想法,因为它们能够在没有上下文丢失的情况下进行传播。

@Griffork好吧,就像我说的我理解你的担忧,我希望你理解我的。
现在我有不同的意见,技术在变化,没有什么是稳定的(围绕 angular 2 的整个戏剧已经证明了这一点),而且我确实更喜欢打破事物并拥有更好的工具而不是坚持我拥有的东西,我确实认为在从长远来看,它让我赢得时间和金钱。
现在就像我们早先得出的结论一样,我怀疑我们会在这里达成共识,我更喜欢指令,你更喜欢你的提案。
无论如何,就像我说的那样,我将尝试分叉编译器并添加非空类型,因为我认为这对编译器来说不是一个大的变化。
感谢讨论 很有趣

@fdecampredon

感谢您的讨论

假设你的意思是有启发性的(有趣的作品也:D),我也很喜欢辩论:)。
祝你的 fork 好运,我希望我们在这里有一个 TS 人员至少驳回我们的一些建议(所以我们知道他们绝对不想实施什么)。

因此,为了进一步详细说明我对@RyanCavanaugh所描述的问题提出的解决方案,在添加空类型和--noImplicitNull标志后,类型检查器在没有标志的

每当类型重载包含|null (如给定示例)时:

function fn(x: string): number;
function fn(x: number|null): string;

类型检查器将接受每个重载并在该参数位置创建带有(隐式)null 的新变体,并添加到列表的末尾:

function fn(x: string): number;
function fn(x: number|null): string;
// implicit signature added at the end, caused by the first overload
function fn(x: null): number;

这与严格的--noImplicitNull版本具有相同的解释:当传递空值时,第一个匹配的重载是显式的number|null 。 实际上,它使显式空值比隐式空值更强。

(是的,我知道隐式定义的数量随着具有|null重载的参数数量呈指数增长 - 我希望有一个更快的实现,如果没有其他类型,则第二次“允许”空值匹配,但我认为无论哪种方式都不会成为问题,因为我预计这种情况很少见)

像这样制定,我认为该更改有望完全向后兼容和选择加入。

以下是有关其余行为的更多想法:

当未传递标志时,任何类型的值也允许分配 null 或保留未初始化的值。

当该标志打开时,将允许保留未初始化(未分配)或 null 的值,直到它们传递给期望它们不为 null 的函数或运算符为止。 不确定班级成员:他们要么

  • 需要在构造函数的末尾初始化,或者
  • 可以通过检查是否调用了完全初始化它们的成员方法来跟踪初始化。

在两种模式下,显式空联合的行为与所有其他联合相似。 他们需要一个守卫来缩小联合类型。

我刚刚阅读了_所有_上面的评论,内容很多。 令人失望的是,经过这么多评论和 5 个月后,我们仍在争论相同的观点:语法和标志......

我认为很多人都对编译器在大型代码库中进行静态空值检查的价值提出了强有力的论据。 我不会再说什么(但+1)。

到目前为止,主要的争论是关于语法和与大量现有 TS 代码和 TS 库定义的兼容性问题。 我可以理解为什么引入 null 类型和 Flow 语法?string似乎是最干净和最自然的。 但这改变了string的含义,因此对 _all_ 现有代码来说是一个_巨大的_重大变化。

上面针对这一重大更改提出的解决方案是引入编译器标志( @RyanCavanaugh很好地说明了为什么这是一个糟糕的主意)或每个文件的指令。 这似乎是一个非常黑客的权宜之计。 此外,我讨厌在 diff 审查或大文件中阅读代码,并且无法立即知道_肯定_我正在查看的代码的含义。 'use strict';被引用为先例。 不是因为TS继承了JS过去的错误,我们就应该重蹈覆辙。

这就是为什么我认为“not null”注释( string!!string ,...)是唯一的方法,我将尝试在讨论中引入一个新的论点:也许是没那么糟糕。

没有人希望在整个代码中都有刘海( ! )。 但我们可能不必。 TS 推断了很多关于打字的信息。

1.局部变量
我不希望用刘海来注释我的局部变量。 我不在乎是否将null分配给局部变量,只要我以安全的方式使用它,TS 完全能够断言。

var x: string;  // Nullable
x.toString();  // Error: x can be undefined or null
x = "jods";
x.toUpperCase();  // Fine.
notNull(x);  // Fine

我经常使用初始化程序而不是类型。

var x = "jods";  // x: string (nullable)
notNull(x);  // Fine
x = null;  // Fine
notNull(x);  // Error

2. 函数返回值
我经常不指定返回值。 正如 TS 推断类型一样,它可以推断是否可以为 null。

function() /* : string! */ {
  return "jods";
}

编辑:由于继承和函数变量的坏主意,请参阅下面@Griffork 的评论
如果我给出返回类型,TS 完全能够为我添加“非空”注释。

如果您想覆盖推断的可空性(例如,因为派生类可能会返回 null,尽管您的方法没有),您可以明确指定类型:

function f() : string {  // f is nullable although this implementation never returns null.
  return "abc";
}

3. 功能参数
就像您必须明确指定类型一样,这就是您_必须_指示您是否不接受空参数的地方:

function (x: string!) { return x.toUpperCase(); } // OK
function (x: string) { return x.toUpperCase(); } // Error

为了避免旧代码库中的编译错误,我们需要一个新标志“空解引用是一个错误”,就像我们有“没有隐含任何”一样。 _这个标志不会改变编译器分析,只会报告错误!_
要添加很多刘海吗? 如果不对真实的大型代码库进行统计,就很难说。 不要忘记可选参数在 JS 中很常见,而且它们都是可为空的,因此如果“not null”是默认值,您将需要这些注释。
那些刘海也是很好的提醒,调用者不接受 null,这与他们今天对string了解一致。

4. 类字段
这类似于3。如果您需要读取一个字段而不能推断其内容,则需要在声明时将其标记为不可为空。

class C {  x: string!; }

function(c: C!) // : string! is inferred
{ return c.x; } // OK, but annotations are required

我们可以想象一个初始化程序的简写,也许:

class C {
  x! = "jods"; // Note the bang: x is inferred as !string rather than just string.
}

如果您说 not-null 是初始值设定项的默认值,则可空属性可能需要类似的速记。
同样很难说现有代码中的大多数字段是否可以为空,但是这个注释非常符合开发人员今天的期望。

5. 声明, .d.ts
巨大的好处是所有现有的库都可以工作。 它们接受空值和非空值作为输入,并假定它们返回一个可能的空值。
起初,您需要明确地将一些返回值强制转换为“非空”,但随着定义缓慢更新以指示它们何时从不返回空,情况会有所改善。 注释参数更安全,但不是成功编译所必需的。

我认为声明编译器无法推断的值的“非空”状态可能很有用(例如,在操作无类型代码时,或者当开发人员可以对程序的全局状态做出一些断言时)和速记可能不错:

var a: { s: string } = something(); // Notice s is nullable.
notNull(a.s); // Error
notNull(<!>a.s);  // OK, this is a shorthand "not-null" cast, because the dev knows about something().

我认为能够以一种简单的方式提示编译器某些东西不为空是可取的。 默认情况下对于 not-null 也是如此,尽管使用简单的逻辑语法更难。

对不起,这是一个很长的帖子。 我试图证明即使使用“默认为 null”,我们也不必注释所有代码:TS 可以在没有我们帮助的情况下推断大多数代码的正确性。 两个例外是字段和参数。 我认为这并不过分嘈杂,并且可以提供良好的文档。 支持这种方法的一个优点是它向后兼容。

我同意你在@jods4所说的大部分内容,但我有一些问题和疑虑:

2) 如果编译器可以在返回类型上自动将可空转换为不可空,您如何覆盖它(例如,对于继承或被替换的函数)?

3 & 5) 如果编译器可以将可空转换为不可空,那么代码文件中的函数签名现在可以与 d.ts 文件中的相同函数签名表现不同。

6) 使用联合类型时,爆炸如何工作/看起来如何?

2)好抓。 我认为它确实不适用于继承(或“委托”)。
我在 TS 方面的经验是,我们几乎从不明确键入函数的返回值。 TS 算出它们,我认为 TS 可以算出空/非空部分。

我们明确指定它们的唯一情况是当我们想要返回接口或基类时,例如为了可扩展性(包括继承)。 可空性似乎非常符合这种描述。

让我们改变我上面的提议:一个显式类型的返回值是 _not_ 推断不可为空,如果它没有如此声明。 这适用于您建议的情况。 它提供了一种方法来覆盖编译器在子类中可能为空的合同上推断“非空”。

3 & 5) 有了上面的变化,情况就不是这样了,对吧? 代码和定义中的函数签名现在的行为方式完全相同。

6)有趣的问题!
这确实不是很好,特别是如果您将泛型添加到组合中。 我能想到的唯一出路是所有部分都必须是非空的。 由于泛型,您可能需要一个空类型文字,请参阅我的最后一个示例。

var x: number | string!; // compiler error
var x: number | string; // x can be null
var x: number! | string!; // x cannot be null
function f<T>() : number | T; // f can be null
function f<T>() : number! | T; // f is nullable if T is nullable
function f<T, G>(): T | G; // f is nullable if T or G is nullable
function f<T>(): T | null; // f is nullable even if T is not nullable.
function f<T>(): T!; // Whatever T is, f never returns null.

// Generics constraint option 1
function f<T!>(x: T!, y: T): T!; // T: not nullable type, x: not-null, y: null, f: not-null
function f<T!>(x: T!, y: T): T;  // T: not nullable type, x: not-null, y: null, f: null

// Generics constraint option 2
function f<T!>(x: T, y: T | null): T; // same as option 1.1
function f<T!>(x: T, y: T | null): T | null;  // same as option 1.2

为了便于讨论,请注意,如果默认情况下使用非空类型,则还需要发明新的语法:如何表达泛型类型不得允许为空值?
相反,您将如何指定即使泛型类型 T 包含空值,函数也永远不会返回 T 但永远不会返回空值?

类型文字还可以,但也不是很令人满意: var x: { name: string }! ,尤其是当它们跨越多行时:(

更多例子:
非空数字数组number![]
非空数组: number[]!
什么都不能为空: number![]!
泛型: Array<number!>[]

2, 3 & 5) 我同意对提案的更改,这似乎是可行的。
6)

function f<T>() : number | T; // f can be null
function f<T>() : number! | T; // f is nullable if T is nullable

我假设在这里你的意思是 f 可以返回null,而不是 f 可以分配给 null。

对于联合,您认为以下语法是另一个可用的选项吗? (看起来有点傻,但在某些情况下可能更简洁/更容易理解)。

var x = ! & number | string;

表示两者都不可为空。


对于类型文字,我会说您可以将 bang 放在文字表达式的开头和结尾,因为var x: !{name: string}在我看来比在结尾更容易使用。

@jods4你有没有读到我的“显式空值优先于隐式空值”的建议修复? 我认为,这解决了不同的语义问题。

@格里福克
是的,当然,“可以为空”是“可以返回空值”的错误措辞。

这让我觉得function f() {}实际上是f的声明,可以稍后设置...我们是否要表达f永远不会设置为 null ?喜欢function f!() {}吗?在我看来,这似乎太过分了。我认为类型检查员应该假设情况总是如此。还有其他不常见的边缘情况,无论如何它都无法处理。

关于新选项,我同意它看起来有点傻,并在类型声明中引入了一个额外的符号: & ,只有一个目的......对我来说这看起来不是一个好主意。并且在多种语言中, & AND绑定的优先级高于| OR ,这不是这里发生的情况。

将 bang 放在类型前面是一个可能的替代方案,我想我们应该尝试重写所有示例,看看什么看起来更好。我认为我建议的这个字段快捷方式将是一个问题:

class C {
  name! = "jods";  // Field inferred string, but marked not nullable.
  // Maybe we could do that instead, which works with "!T" convention:
  name : ! = "jods";
  // That kind of make sense with the proposed <!> cast.
}

@spion
是的,我看见它了。

首先,我不确定它是否真的解决了手头的问题。那么根据您的新定义,哪个重载绑定到fn(null) ?返回number的“生成”?这与第二个重载也接受null但返回string的事实不一致吗?还是因为无法执行重载解析而导致编译器错误?

其次,我很确定我们可以提出很多其他问题。我认为在切换开关时更改源代码的全局含义是行不通的。不仅在.d.ts ,而且人们喜欢重用代码(库重用甚至复制粘贴)。你的建议是我的源代码的语义取决于编译器标志,这对我来说似乎是一个非常有缺陷的命题。

但是如果你能证明代码在所有情况下都能正确编译和运行,我完全赞成!

@jods4

在这两种情况下,将使用第二个重载

function fn(x: string): number;
function fn(x: number|null): string; 

因为第二个显式 null 将优先于隐式(第一个),无论使用哪个标志。 因此,含义不会根据标志而改变。

由于 null 类型现在不存在,它们将被一起引入并且更改将完全向后兼容。 旧的类型定义将继续正常工作。

@spion
那么隐含的function fn(x: null): number;怎么回事? 如果第二个重载总是优先,那么无论使用哪个标志,整个“修复”都没有任何效果?

其他问题:今天所有的 lib 定义都有“允许为空”的行为。 因此,在它们_all_ 更新之前,我必须使用“隐式空”标志。 现在有了这个标志,我怎样才能在我的项目中声明一个采用非空参数的函数?

// null by default flag turned on because of 3rd party libs.
function (x: string)   // <- how do I declare this not null?
{ return x.toUpperCase(); }

修复肯定有效果。 它使带有和不带有标志的行为保持一致,从而解决了上述语义问题。

其次,您不必为 3rd 方库使用“默认为空”标志。 此功能最重要的一点是,在绝大多数情况下,您无需做任何事情即可获得好处。

假设有一个库可以计算字符串的字数。 它的声明是

declare function wordCount(s: string): number;

假设您的代码以这种方式使用此函数:

function sumWordcounts(s1:string, s2:string) {
  return wordCount(s1) + wordCount(s2);
}

该程序通过两个编译器:即使隐式空值被禁用。 代码完全向后兼容。

为什么? 因为今天的编译器已经相信值在您尝试使用它们的任何地方都不是空的(即使它们理论上可以为空)。

当您尝试使用这些值时,新编译器还会假定这些值不为空。 没有理由不相信,因为您没有指定该值可能为空,因此部分行为保持不变。 行为的变化仅在分配 null(或使变量未初始化)然后尝试将这些值传递给上述函数时才生效。 它们不需要对正常使用值的代码的其他部分进行任何更改。

现在让我们看看wordCount

function wordCount(s) {
  if (s == '') return null;
  return s.split(' ').length
}

糟糕,这种类型并不能说明全部情况。 该函数可能返回空值。

问题正是如此。 即使我们想要,也不可能在当前的编译器中讲述整个故事。 当然,我们说返回值是一个数字,它隐含地声明它可以为空:但是当我们尝试错误地访问可能为空的值时,编译器从不警告这一点。 它总是很高兴地认为它是一个有效的数字。

noImplicitNull更改后,如果我们使用相同的类型定义,我们将在这里得到完全相同的结果。 代码将毫无怨言地编译。 编译器仍然会很高兴地认为 wordCount 总是返回一个数字。 如果我们传递空字符串,程序仍然会失败,就像以前一样(旧编译器不会警告我们返回的数字可能为空,新编译器也不会,因为它信任类型定义)。 如果我们想要相同的行为,我们可以保留它而无需更改代码中的任何内容。 (1)

但是现在我们将_能够_做得更好:我们将能够为 wordCount 编写改进的类型定义:

declare function wordCount(s:string):string|null;

并在我们尝试使用wordCount而不检查 null 返回值时收到编译器警告。

最好的部分是这种改进是完全可选的。 我们可以保持所有类型声明的原样,并获得与现在几乎完全相同的行为。 类型声明不会变得更糟 (2) - 只有在我们认为有必要的情况下才能将它们改进为更精确。


(1):即使没有使类型定义更好,这里也已经有了改进。 使用新的类型定义,您将不会意外地将 null 传递给sumWordcounts并在wordCount函数尝试将.split()设为 null 时获得空指针异常。

sumWordcounts(null, 'a'); // error after the change

(2): 好吧,那是谎言。 对于一小部分函数来说,类型声明会变得更糟:那些采用非可选参数的可为空参数的函数。

对于可选参数,事情仍然没问题:

declare function f(a: string, b?:string); 

但不适用于非可选且可为空的参数

declare function f(a: string, b:string); // a can actually be null

我认为这些功能非常罕见,所需的修复也很少。

@spion

修复肯定有效果。 它使带有和不带有标志的行为保持一致。

哪种方式,你能举一个完整的例子吗? 你还说:

在这两种情况下,将使用第二个重载

对我来说,这意味着修复没有效果?

该程序通过两个编译器:即使隐式空值被禁用。 代码完全向后兼容。

是的,它在两种情况下都没有错误地编译,但结果不同。 sumWordCounts()将在一种情况下输入为number! ,在第二种情况下输入为number? 。 用 switch 改变代码的语义是非常_不_可取的。 如前所述,此更改可能会产生全局影响,例如对重载解析。

当您尝试使用这些值时,新编译器还会假定这些值不为空。 没有理由不相信

不! 这就是重点:当我使用潜在的空值时,我希望编译器向我抛出错误。 如果它“假设”不为空,就像我在编码时所做的那样,那么这个功能就没用了!

行为的变化仅在分配 null(或使变量未初始化)然后尝试将这些值传递给上述函数时才生效。

不确定我是否理解,因为“上述函数”实际上接受空值......

阅读您最后一条评论的结尾,我的印象是,在我们打开开关之前,我们不会获得真正的静态空分析,而在 _all_ 您的库定义被修复之前,您无法这样做。 :(

一般来说,很难完全理解所有情况以及一切如何仅用文字进行。 一些实际的例子会很有帮助。

这是一个示例以及我希望编译器的行为方式:

假设一个库具有从不返回 null 的函数yes(): string!和可能返回 null 的no(): string?

.d.ts定义是:

declare function yes(): string;
declare function no(): string;

我希望能够在 TS 中创建大型项目,这些项目受益于静态检查的空值并使用现有的库定义。 此外,我希望能够 _progressively_ 过渡到所有库都使用正确的空性信息更新的情况。

使用上面提议的非空修饰符! ,我可以做到:

function x(s: string!) {  // inferred : string, could be explicit if we want to
  return s.length === 0 ? null : s;  // no error here as s is declared not null
}

x(no());  // error: x called with a possibly null parameter
y(<!>yes());  // no error because of not null cast. When .d.ts is updated the cast can be dropped.

这将如何与您的想法合作?

这个线程是一个过长的讨论(130 条评论!),这使得很难理解每个人提到的想法、建议和问题。

我建议我们用我们的建议创建外部要点,包括建议的语法、TS 接受的内容、错误的内容等等。

@spion因为您的想法涉及编译器标志,所以对于每段代码,您应该使用标志设置和未设置来记录 TS 行为。

这是!T非空标记的要点:
https://gist.github.com/jods4/cb31547f972f8c6bbc8b

它与我上面的评论大致相同,但有以下区别:

  • 我注意到编译器可以安全地推断出函数参数的“非空”方面(感谢@spion的洞察力)。
  • 我加入了@Griffork评论,特别是关于推断返回值的评论,我尝试了!T而不是T!
  • 我添加了一些部分,例如关于构造函数的行为。

随意评论、分叉和创建替代提案( ?T )。
让我们努力推动这一进程。

@jods4

在这两种情况下,将使用第二个重载

对我来说,这意味着修复没有效果?

  1. 这意味着更改重载解析以使两个标志的语言语义一致。

是的,它在两种情况下都没有错误地编译,但结果不同。 sumWordCounts() 将被输入为数字! 在一种情况下和数量? 在第二。 用 switch 改变代码的语义是非常不可取的。 如前所述,此更改可能会产生全局影响,例如对重载解析。

  1. 我的提案中没有number! 。 在这两种情况下,类型都只是number 。 这意味着重载解析继续正常工作,除了空值,在这种情况下,显式空值优先于隐式的新行为以向后兼容的方式规范化语义。

不! 这就是重点:当我使用潜在的空值时,我希望编译器向我抛出错误。 如果它“假设”不为空,就像我在编码时所做的那样,那么这个功能就没用了!

  1. 我试图表达的观点是该功能几乎没有向后不兼容(在类型声明方面)。 如果你不使用它,你会得到与以前完全相同的行为。 如果你想用它来表达可能返回空类型值的可能性,现在你可以了。

就好像编译器之前对objectstring类型的值使用了string类型,允许所有对象上的所有字符串方法并且从不检查它们。 现在Object将有一个单独的类型,您可以开始使用该类型来表示字符串方法并不总是可用。

不确定我是否理解,因为“上述函数”实际上接受空值......

我们来看看这个函数:

function sumWordcounts(s1:string, s2:string) {
  return wordCount(s1) + wordCount(s2);
}

在新的编译器标志下,您将无法使用空值调用它,例如sumWordCounts(null, null); ,这是唯一的区别。 函数本身会编译,因为wordCounts的定义说它接受一个字符串并返回一个数字。

阅读您最后一条评论的结尾,我的印象是,在我们打开开关之前,我们不会获得真正的静态空分析,而在您的所有库定义都已修复之前,您无法做到这一点。 :(

除了检查它们是否为空/未定义并抛出错误以防止该值在整个代码库中传播到完全不相关的地方之外,绝大多数代码并没有真正处理空值或未定义值,这使其变得困难调试。 有一些函数在这里和那里使用可选参数,它们是可识别的,并且可能可以使用新开关进行相应建模。 不像我的例子那样,函数返回空值的情况并不常见(我使用它只是为了说明该功能,而不是作为代表性案例)

我要说的是,绝大多数类型定义将保持正确,对于我们需要修复的少数剩余情况,如果我们认为 null 情况不重要,我们可以选择不修复它们,或者如果我们关心,请相应地更改类型。 这完全符合 typescript 的目标,即在我们需要时逐步“调高”以获得更强的保证。


好的,所以现有的类型定义是

declare function yes(): string;
declare function no(): string;

假设您的代码是在添加该功能之前编写的:

function x(s: string) {
  return s.length === 0 ? null : s;
}

在新编译器下,行为将与旧编译器完全相同

x(no());  // no error
x(yes());  // no error

除非你尝试一些编译器知道可能为空的东西,比如 x() 的结果

x(x(no())) // error, x(something) may be null

您查看no() ,您可以确定它返回 null 的情况很少见,因此您不打算对其进行建模,或者您可以修复类型定义。

现在让我们看看您的提议会发生什么:甚至涉及外部库的每一行代码都会中断。 具有上述完全有效类型定义的函数也会中断。 您必须在任何地方更新每个参数注释并添加!以让编译器停止抱怨,或者在任何地方添加空检查。

底线是 TypeScript 中的类型系统当前是错误的。 它对空值有双重解释:

当我们尝试将 null 分配给T类型的某个变量,或者将 null 作为需要类型T的参数传递时,它的行为就好像类型是T|null

当我们尝试使用T类型的值时,它的行为就好像类型是T并且不可能为 null。

您的提议建议始终将所有正常类型T视为T|null ; 我的建议只是将它们视为T 。 两者都改变​​了语义。 两者都使编译器行为“正确”

我的论点是,“只是T ”变体比它看起来更痛苦,并且与绝大多数代码保持一致。

编辑:我刚刚意识到您的建议可能是继续处理没有“!”的类型。 或者 ”?” 和以前一样。 那是向后兼容的,是的,但是要获得任何好处需要做很多工作(因为除了检查/抛出之外,绝大多数代码根本不处理空值。

@spion

1.这意味着改变了重载解析以使两个标志的语言语义一致。

你只是在陈述同样的事实,但我仍然不清楚它解决了哪种情况以及以何种方式。
这就是为什么我要一个具体的例子。

1.我的提案中没有number! 。 在这两种情况下,类型都只是number

那是不正确的,我知道语法不同,但基本概念是相同的。 当我说number!我强调的是非空数字类型,在您的提案中,它只是number 。 在您的情况下,第二种情况不是number ,而是number | null

除了检查它们是否为空/未定义并抛出错误以防止该值在整个代码库中传播到完全不相关的地方之外,绝大多数代码并没有真正处理空值或未定义值,这使其变得困难调试。 有一些函数在这里和那里使用可选参数,它们是可识别的,并且可能可以使用新开关进行相应建模。 不像我的例子那样,函数返回空值的情况并不常见(我使用它只是为了说明该功能,而不是作为代表性案例)

我认为像这样的描述使_很多_假设和捷径。 这就是为什么我认为要取得进展,我们需要转向更具体的示例,包括代码、语法和系统工作原理的解释。 你说:

除了检查它们是否为空/未定义并抛出错误之外,绝大多数代码并没有真正处理空值或未定义值

很有争议。

有一些函数在这里和那里使用可选参数。

更值得商榷。 JS 库中充满了这样的例子。

,它们是可识别的,并且可能可以使用新开关进行相应建模

提出更具体的命题,因为这是一条巨大的捷径。 我想我可以看到实际 JS 代码的走向,但是您如何“识别” declare function(x: {});interface的可选参数?

像我的例子那样,函数返回空值的情况并不常见。

再次很有争议。 许多函数返回nullundefined值: find (未找到项目时)、 getError (没有错误时)等等...如果您想要更多示例,只需查看标准浏览器 API,您会发现_很多_。

我要说的是绝大多数类型定义将保持正确。

从我之前的评论中可以看出,我不相信这一点。

对于我们需要修复的少数剩余情况,如果我们认为 null 情况不重要,我们可以选择不修复它们,或者如果我们关心,则相应地更改类型。 这完全符合 typescript 的目标,即在我们需要时逐步“调高”以获得更强的保证。

在这一点上,这句话对我来说似乎并不微不足道。 你能举出具体的例子来说明它是如何工作的吗? 尤其是 _progressively_ 部分。

现在让我们看看您的提议会发生什么:甚至涉及外部库的每一行代码都会中断。 具有上述完全有效类型定义的函数也会中断。 您必须在任何地方更新每个参数注释并添加!以让编译器停止抱怨,或者在任何地方添加空检查。

这几乎是完全错误的。 请仔细阅读我写的要点。 您会注意到实际上几乎不需要非空注释。 但是有_一个_重大变化,是的。

假设您使用一个使用库定义的现有代码库,并尝试在不更改任何代码的情况下使用我的建议对其进行编译:

  • 即使没有注释,您也将获得很多空分析的好处(与在 Flow 编译器中获得的方式非常相似)。
  • 一点会中断:使用您知道不会返回 null 的库函数的返回值。
declare function f(): string; // existing declaration, but f will never return null.
var x = f();
x.toUpperCase();  // error, possibly null reference.

长期修复是更新库定义以使其更准确: declare function f(): !string;
短期修复是添加一个非空类型转换: var x = <!>f();
为了帮助具有大量依赖项的大型项目更轻松地升级,我建议添加一个类似于“无隐含任何”的编译器标志:“将可能的空引用视为警告”。 这意味着您可以使用新的编译器并等待库更新定义。 注意:与您的命题不同,此标志不会更改编译器语义。 它只更改报告为错误的内容。

正如我所说,我不确定我们是否在这场辩论中取得了进展。 我建议你看看我的要点,并在你的旗帜的两种状态下用你自己的想法、代码示例和行为创建一个。 由于我们都是编码员,我认为这会更清楚。 还有很多边缘情况需要考虑。 这样做之后,我有很多问题要问特殊情况,但如果没有精确的定义,讨论概念和想法真的没有用。

在这个 130 多条评论问题中,有太多关于不同事情的讨论。

我建议我们在这里继续_一般_讨论。

对于可能在 TS 中实现非空类型的具体提案的讨论,我建议我们打开新问题,每个问题都关于一个设计提案。 我创建了 #1800 来讨论!T语法。

@spion我建议你也为你的设计创建一个问题。

许多人想要非空类型。 在新语言(例如 Rust)中很容易,但很难在现有语言中进行改造(人们长期以来一直在 .net 中要求非空引用——我认为我们永远不会得到他们)。 看看这里的评论表明这是一个重要的问题。
Flow 已经说服我这可以为 TS 做到,让我们努力实现它!

@jods4我很抱歉这么说,但你的提议与非空类型无关,它更多地与某种控制流分析有关,几乎不可预测,并且完全破坏了逆向兼容性(实际上大部分代码根据您的要点中描述的规则,有效 1.4 ts 将失败)。
我同意讨论无处可去的事实,而且 130 条评论+ 可能太多了。 但也许是因为这里有两组人在讨论:

  • 那些认为非空类型应该是默认值并希望找到一种方法来实现它的人(通过标志或任何其他机制)
  • 那些不想要非空类型并且只是尽量避免引入它们,或者将它们推入新语法的人。

这两群人早就停止了相互的争论,最后我们需要的是TS团队的观点,在那之前我个人会停止尝试更多地讨论这个话题。

@fdecampredon
关于我的要点部分,我将您的论点复制粘贴到 #1800 中,如果您真的感兴趣,我们可以讨论为什么它在那里是一个糟糕的设计。 我不太明白你的观点,但我不想在这里开始讨论,这就是我首先创建 #1800 的原因。

关于这个话题,我同意你的看法,讨论无处可去......

从我之前的评论中可以看出,我不相信这一点。

好吧,我真的不知道如何以任何其他方式解释它——我已经解释过几次了。 让我再试一次。

现在你无法表达

declare function err():string;

返回空值。 这是不可能的,因为打字稿总是很乐意让你做err().split(' ') 。 您无法说“这可能无效”。

在此更改之后,您将能够通过将string更改?stringstring|null

但是如果你不这样做,_你不会失去任何你在改变之前拥有的东西:

您在更改之前没有进行空检查,之后也没有(如果您什么都不做)。 我认为这主要是向后兼容的。 当然,仍有待在更大的代码库上进行演示。

不同之处在于:您不能在更改之前强制进行空检查,但是您可以_在更改之后(逐渐地,首先在您最关心的定义上,然后在其他地方)

@fdecampredon我仍然在讨论这个主题,因为我觉得对这个问题以及利用该功能的“困难”和“严格”有很多误解。 我怀疑很多潜在的问题都被严重夸大了。 我们可能应该尝试在 fork 中实现--noImplicitNull的基本变体 - 我不确定我能否在下一个时期找到时间,但如果我这样做了,我会试一试,听起来是个有趣的项目。

@spion ,我完全

@fdecampredon我唯一的要求是不要进行大的

我猜在你当前的例子中@spion你几乎可以像我们现在所做的一样继续编码 Typescript,如果你假设一个局部变量如果没有被分配到被转换为可空和带有 ? 也可以为空。 好吧,除了您之前使用可为空的早期函数参数的那个示例。

@spion
我从你上一条评论中所理解的与我之前所理解的大不相同......

所以: string是“永远不会检查空值的旧字符串类型,就像永远不会检查类型正确性的<any> ”。

新语法: string | null向后兼容,因为它之前不存在并且在没有空检查的情况下访问此类型是一个错误。

这很有趣,而且很难理解所有的含义。 我得考虑一段时间。

想到的第一个问题是如何对输入值(例如函数参数)施加约束:

declare f(x: string): void;
f(null);

这是正常还是错误? 如果没问题,我怎么会出错。

_我仍然认为这个讨论中的任何想法都丢失了,你不想用你的这个想法打开一个新的问题吗? 我们可以在那里讨论。_

@jods这将是一个错误:

declare f(x: string): void; //string must not be null.
declare f(x: string|null): void; //string may be null (not sure about undefined here).
declare f(x?: string): void; //I assume x may be null or undefined.

@jods4我认为我们不需要在一个主题上创建多个问题,然后事情会变得更难跟踪,因为您必须查看 10 个不同的提案才能查看是否发生了任何事情/订阅 10 个不同的提案. 而且他们都必须在他们的介绍中包含指向其他提案的链接,这样每个寻求不可为空性的人都不会只看到并投票给一个。

@fdecampredon我知道有很多帖子和很多未解决的问题,但这并不意味着我们应该停止尝试找到每个人都喜欢的解决方案。 如果您不喜欢我们仍然很乐意谈论这个并解释我们自己的事实,那么我们非常欢迎您取消订阅该主题。

@格里福克
但只有在你打开魔法旗之后,对吗?
因此,关闭标志后,您将获得未经检查的代码,除了以前不存在的新的可空类型; 那么当你打开标志时,所有类型都是不可为空的并被检查?

我认为最终结果可能是最好的解决方案。 _但它几乎破坏了今天存在的所有代码。_ 想象一下,我有 30 万行代码......我必须在类型实际上可以为空的任何地方添加一个可为空的注释,然后才能打开标志。 这是一个大问题。

如果我理解正确,这对新项目很好(一旦.d.ts更新)但对现有代码来说非常痛苦。

@Griffork我对这个单一问题的问题及其长长的评论是,很难对单个提案进行集中讨论。 订阅 3 或 4 期并不是什么大问题,而且每一期都有很好的背景。

@spion的原始提案没有魔法标志。 这是打字稿工作方式的核心变化。

是的,但参与此讨论的人应该阅读之前的所有内容。 如果您认为人们不应该阅读之前的所有内容(因此可能会提出相同的建议)_那么_我们可以将其拆分; 但我不同意。 它集中在一个地方的意义在于,尽管我们在谈论不同的细节或不同的可能实现,但我们都在谈论同一件事
同一个话题,不是不同的话题。
所有的对话都是集中的,没有任何对话会因为没有得到回应而被埋没。

你也不能真正按照实施细节来分解这个对话,因为很多提案通常会涉及当前正在讨论的许多不同的细节。

将提案彼此分开意味着如果有人发现缺陷,他们必须转到多个不同的线程并编写它。 人们不会从彼此的错误中吸取教训,人们有能力(他们会)将自己隔离在一个讨论中,并重复其他讨论中概述的错误。

关键是:拆分对话会导致人们不得不重复很多帖子,因为观众懒得阅读所有其他链接帖子中的所有内容(假设所有相关帖子都链接了所有其他帖子)。

另外@jods4当您将 null 分配给某物时,这只是一个重大更改。

Imo 与您的提案相比,破坏性更改要少得多,这将使每个不应将 null 分配给它的定义都需要检查/触摸,然后才能使用它们并获得不可为空类型的好处。

以下是@jods4@spion建议的“升级”过程的示例(假设打开了所有必要的标志):

function a(arg1: string): string;
a("mystr").toLowerCase(); //errors in <strong i="11">@jods4</strong>'s proposal because a may return null.
a("mystr").toLowerCase(); //fine in <strong i="12">@spion</strong>'s proposal.

a(null).toLowerCase(); //fine in <strong i="13">@jods4</strong>'s proposal because a may accept null.
a(null).toLowerCase(); //errors in <strong i="14">@spion</strong>'s proposal since a doesn't accept null.

最终,他们都在打破变革。 它们都同样难以调试,错误发生在变量使用而不是变量声明。 不同之处在于 null 分配给某物时的一个错误 (@spion),而 null 未分配给某物时的另一个错误 (@jods4)。 就我个人而言,与不将 null 分配给事物时相比,我将 null 分配给事物的频率要低得多,因此@spion对我来说几乎没有什么变化。

其他我喜欢的东西:

  • 变量的类型不会动态改变。 如果我想要,我会使用流程:
var s: string;
s.toUpperCase(); // error
var t = (s || "test").toUpperCase(); // ok. Inferred t: string
yes(t);  // ok
t = no();
yes(t);  // error

就我个人而言,如果我调用 no(),我会想要一个错误,而不是类型的隐式更改。

  • 包含单词 null 的可空值,而不是一些符号,它更容易阅读,更难记住。 真的 ! 应该用于“not”而不是“not-null”。 我讨厌必须记住符号的作用,就像我讨厌首字母缩略词一样,我永远无法记住它们并且它们显着降低了我的工作速度。

@jods4我知道人们很难理解当前的对话,但是将其拆分为单独的问题无济于事,因为我们最终将不得不在所有这些问题中提出所有这些要点,因为uninformed 会问和我们一样的问题。 这真的不会_帮助_对话,它只会使维护和跟上变得更加困难。

@Griffork老实说,我阅读了所有内容,但我怀疑很多人

每次你有一些可以为空的东西(这意味着它在某个时候被分配为空,否则它不会真的可以为空)时,这是一个破坏性的变化。 将在将 null 分配/传递给函数的情况下报告错误,但修复位于声明处。 这包括函数声明、变量声明、类中的字段声明......很多东西都可能需要审查和修改。

@jods4未实现的子讨论应该已经分支,而不是实际的核心信息。 但是现在回去改变它为时已晚。

@spion我想我有一个更简单的解决方案来解决@RyanCavanaugh问题的“-nonImplicitNull”标志,这个想法很简单,我们只需要不允许?type type|nulltype|undefined没有这个标志。

@fdecampredon那么您需要每个可用库的两个版本并保持最新。
如果我们有一个非严格的项目来尝试代码和运行依赖于更严格项目的代码的测试,它也会终止我们的流程。

@jods4

declare f(x: string): void;
f(null);

使用建议的--noImplicitNull标志时,是的,这将是一个错误。

我现在还不太愿意公开正式提案,我想至少做一些更多的思想实验,或者尝试在一个分支中实现一个更简单的想法版本。

我注意到!T提案实际上也会破坏很多代码。 不是因为现有类型的语义发生了变化,而是因为新的分析会在很多地方引发错误。

所以我关闭了它。 如果我们破坏了很多代码,我更喜欢另一种方法,它看起来更干净。

我现在确信没有办法在不破坏大量现有代码的情况下引入这个特性。 这对于尚未编写的所有代码仍然有好处,并且可能旧代码可以继续编译而无需编译器检查空值的好处——这正是今天的情况。

我想知道官方 TS 团队对于这与它的好处的破坏方面的立场是什么。

我们不太可能像“默认情况下使所有内容都不可为空”那样大休息。 如果有一些提案会_仅_在明显是错误的情况下发出新错误,那可能会摆在桌面上,但是这样的提案很难获得 :wink:

如果影响范围较小——例如,在传递null作为参数时重载决议的变化,例如,确切的行为不一定立即显而易见,那可能是一个不同的故事。

:+1: 检测到正常人: @RyanCavanaugh

不改变现有的工作方式,但为联合引入空类型,这迫使您在使用变量之前保护它(或将其分配给非空类型的变量)怎么样? 默认行为仍然允许将 null 分配给正常类型的变量以及使用 null 类型的变量,但是如果正确标记可能返回 null 的函数将强制强制转换。

这样就不会发生重大变化,但是通过良好的实践,您仍然可以通过类型系统通过推断类型和良好的编码实践发现一堆错误。

然后(也许稍后?)您可以添加一个 --noImplicitNullCast 标志,以防止将 null 分配给在键入时没有 null 的变量(这或多或少类似于@spion启用标志的建议)。

我想这与所有正常输入和添加 --noImplicitAny 标志不会有太大不同。

@格里福克
包的第一部分(添加null类型并禁止在没有保护的情况下取消引用)不是 100% 兼容。 你会破坏比你想象的更多的代码。 考虑一下:

有了这个新功能,许多 lib defs 将被更新,包括内置的。 假设他们更改了一些签名以明确包含 null 作为可能的返回值。 一些例子:

interface Array<T> {
  find(predicate: (T) => bool) : T | null;
  pop() : T | null;
}
interface Storage {
  getItem(key: string) : any | null;
}

有许多这样的API: find返回undefined如果没有数组元素匹配谓词, pop返回undefined如果数组为空, localStorage.getItem如果未找到密钥, sessionStorage.getItem都返回null

下面的代码是合法的,之前编译的很完美。 它现在会因错误而中断:

var xs: string[];
if (xs.length > 0) return xs.pop().trim();  // error xs.pop() may be undefined (false positive)

var items : { id: number }[];
var selectedId : number;
// Assume we are sure selectedId is amongst items
var selectedItem = items.find(x => x.id === selectedId);
selectedItem.toString(); // error selectedItem may be undefined (false positive)

如果你从localStorage中取出你知道在那里的东西,同样的想法。 有很多这样的代码,现在它需要一个强制转换(或一些新的语法)来编译并告诉编译器:假设这不是空的,我知道我在做什么。

当然,这将包括一些实际的错误。 但它也将包括许多误报,这些都是破坏性的变化。 在 100K+ LOC 中,这不是一个微不足道的编译器升级。

这可能意味着唯一可行的方法是:从头开始编写的新代码或迁移旧代码充分利用空检查; 遗留代码根本没有任何好处。 这是由编译器选项 (--enableNullAnalysis) 驱动的,并且没有“过渡”或“渐进式”迁移路径。

嗯,是。 但是您总是可以冻结项目的 lib.d.ts 或获取旧的。
新类型会破坏旧代码,无论如何都会发生。 几乎所有引入的新类型都需要以某种方式更新旧代码才能正常工作(例如联合、泛型)。
这一变化意味着那些想要忽略更新或冻结他们的库/代码定义的人不会受到影响。
从理论上讲,大多数使用这些函数的代码都应该受到保护(并且通常在大型代码库中)——因为如果你不这样做,你的代码中就会潜伏着错误。 因此,此更改更有可能捕获错误而不是导致广泛的破坏性更改。

编辑
@jods4为什么您使用不安全代码作为示例,而这正是可能导致我们尝试通过进行此更改来检测的错误的代码类型?

编辑 2
如果不打破_some_(错误/不安全)的代码,那么就没有指向做任何改变是否与此话题不断

但是话又说回来,因为这只是一个语法问题,所以一切仍然可以正确编译,它只会到处出错。

_新类型会破坏旧代码,无论如何都会发生。_

不是这种情况。 除了特殊情况,我们不会添加破坏现有代码的新功能。 泛型和联合没有以需要 lib.d.ts 的使用者更新其代码库以使用新版本编译器的方式添加到 lib.d.ts。 是的,人们可以选择使用旧版本的库,但我们不会对破坏大量现有代码的语言进行更改(基本上所有主要语言的情况都是如此)。 偶尔会有例外(https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes),但它们很少,而且很远,如果我们相信我们正在破坏的代码只能是一个错误。

但是您可以随时冻结项目的 lib.d.ts 或获取旧的

冻结 lib.d.ts(以及我可能会使用 BTW 的任何其他 .d.ts)具有非常强烈的影响。 这意味着我无法获得我使用的库或新的 HTML api 的任何更新。 这不是一件可以掉以轻心的事情。

从理论上讲,大多数使用这些函数的代码都应该受到保护(并且通常在大型代码库中)——因为如果你不这样做,你的代码中就会潜伏着错误。

JS 有返回undefined (有时是null )的传统,这在很多情况下会在其他语言中引发错误。 这方面的一个例子是Array.prototype.pop 。 在 C# 中,从空栈中弹出会抛出异常。 所以你可以说它总是返回一个有效的值。 在 Javascript 中弹出一个空数组返回undefined 。 如果您的类型系统对此很严格,您必须以某种方式处理它。

我知道你会回答这个问题,这就是为什么我写了一些合法的、可工作的代码的例子。 在大型代码库中,您会发现很多示例,其中 api 在某些情况下可能返回 null,但您知道它在您的特定情况下是安全的(因此您忽略任何安全检查)。

一个小时前我还在看某个图书馆的微任务部分。 主要部分基本上归结为:

class MicroTasks {
  queue: Array<() => void>;

  flushQueue() {
    while (queue.length > 0) {
      let task = queue.pop();
      task();  // error possible null dereference (not!)
     }
  }
}

有很多这样的案例。

关于您的编辑 2:是的,希望此更改_将_捕获错误并指出无效代码。 我相信它很有用,这就是为什么我希望它以某种方式发生。 但是 TS 团队会考虑 _valid_ 代码中的重大更改。 这里需要做出一些权衡。

@danquirk Fair,那么我想这个功能永远无法实现。
@jods4我明白你在说什么,但如果不破坏代码,就永远无法实现可空值。

伤心,我应该最终关闭那个问题吗?

大概。

我想如果我们避开可为空和不可为空的论点......

可以使用一组新功能以非侵入式方式解决(或缓解)该问题:

  • 引入null-typeguard符号?<type>
    如果一个类型用这个符号注释,那么直接访问它是错误的

``` 打字稿
var foo: ?string;

foo.indexOf('s'); // 错误
foo && foo.indexOf('s'); // 好的
``

  • 引入一个标志--nouseofunassignedlocalvar

``` 打字稿
var foo: 字符串;

foo.indexOf('s'); // 错误

foo = '酒吧';

foo.indexOf('s'); // 好的
``

有趣的历史记录:在尝试为此找到相关问题时,我在 codeplex 上遇到问题在2013 年 2 月提到了--cflowu编译器选项。 @RyanCavanaugh ,我想知道那面旗帜发生了什么?

  • 介绍安全导航操作符#16

TypeScript var x = { y: { z: null, q: undefined } }; console.log(x?.y?.z?.foo); // Should print 'null'

结合起来,这些功能将有助于在使用 null 时捕获更多错误,而实际上不会导致每个人神经衰弱。

@NoelAbrahams
你的第一个建议与我的最后一个完全一样,你只是使用 ? 而不是 |null(阅读@jods4关于更新 lib.d.ts 和破坏性更改的问题的帖子)。

您的第二个提案存在@RyanCavanaugh (上图)之前解决的问题:

改变语言语义的标志是一件危险的事情。 一个问题是影响可能非常非局部:
...[剪辑]...
唯一安全的做法是保持可分配性的语义相同,并更改错误与不依赖标志的内容,就像 noImplicitAny 今天的工作方式一样。

我很确定之前提出了一个没有不同的标志,然后由于@RyanCavanaugh的评论而被放弃。

您的第三个建议与当前主题几乎没有关系(它不再是关于键入和编译时错误,而是关于捕获运行时错误)。 我这么说的原因是因为创建这个主题的原因是为了帮助减少对已知“安全”的变量进行未定义或空值检查的需要(使用一种简单的方法来跟踪),而不是添加新的空值或未定义的检查无处不在。

是否有可能实现建议的其中一项建议,而只是不更新​​ lib.d.ts 以使用可空值(和其他库)? 那么社区可以根据需要维护/使用他们自己的带有可空值的版本吗?

编辑:
具体来说,所有库都包含最新信息,但没有更新它们的类型以需要打字员。

@RyanCavanaugh我会回来讨论如果/当我有一个补丁表明它根本没有那么大的变化(特别是如果 .d.ts 文件没有更新)。

@danquirk类型定义不必更新。 它们可以保持“错误”,并且主要是向后兼容的。

无论如何,这个问题似乎是一个失败的原因。

顺便说一句,MSR 有一个经过修改的编译器,可以做到这一点 - 有没有关于他们的发现的论文? 编辑:找到它: http : 224900但不幸的是,我不确定它是否相关。

@Griffork ,是的,考虑到讨论的长度,我确信几乎所有内容都以一种或另一种形式进行了讨论。 我的总结是关于通过引入许多相关功能来缓解 null 周围问题的实用总结。

不要在任何地方添加新的 null 或 undefined 检查

未分配的局部变量的捕获减轻了这一点,安全导航算子是一个提议的 ES 功能,因此它会在某个时候登陆 TS。

我也认为进入 TS 的概率很低...... :(

我能想到的唯一解决方案是将空安全作为选择加入的编译器标志。 例如,引入新的T | null?T语法,但除非项目选择加入,否则不会引发 _any_ 错误。 因此,新项目获得了好处,选择使其代码与新功能兼容的旧项目也获得了好处。 太大而无法轻松适应的大型代码库没有获得此功能。

也就是说,即便如此,要使此功能生效还有几个问题......

@NoelAbrahams抱歉,我的观点并不是说这是一个不好的建议,只是这不是发起本次讨论所期望的答案。
当(/如果)它成为本机时,我肯定会使用该功能,这听起来非常好。

@jods4与我上面引用的@RyanCavanaugh评论有相同的问题(由于更改标志时其他地方的问题导致的赋值错误)。

同样,最简单的方法是实现其他建议之一(可能是@spion的),而不是将新类型添加到 .d.ts 中。

@格里福克
不一定,尽管这是剩下的“几个问题”之一。 该功能应设计为启用该标志不会_不_改变任何程序语义。 它应该只会引发错误。

例如,如果我们采用“非空” !T设计,我认为我们没有这个问题。 但这远非解决问题。

我还应该指出,虽然T | null是一个重大变化,但?T不是。

@jods4我的印象是“改变语义”包括“改变分配能力”,无论哪种方式,我(可能还有@RyanCavanaugh的)对非局部效应的担忧仍然存在。

@NoelAbrahams怎么样?
T | null 需要类型保护,?T 需要类型保护,它们是相同的东西,但名称/符号不同。
是的,您可以使用标志让它们都“打开”。
我看不出区别?

@Griffork ,我看到它的方式?T只是意味着“不检查空值就不要访问”。 可以随意将此注释添加到新代码中以强制执行检查。 我认为这是一种运算符而不是类型注释。

注释T|null破坏了现有代码,因为它声明了默认情况下类型是否可以为空。

@jbondc
有趣......我还没有深入研究你的提议,但我想我会的。

目前,编译器强制的不变性在 JS 中不像在其他语言中那么有趣,因为它的线程模型禁止共享数据。 但肯定要记住一些事情。

@NoelAbrahams
所有强制执行的提案都在发生重大变化。 甚至你描述的?T 。 我展示了几个例子,为什么上面有一些评论。

这就是为什么我认为如果我们想要它,唯一的出路是让它成为一个编译器选项,并充分考虑这一点,以便启用该选项会改变报告的错误而不是程序行为。 不确定它是否可行。

我实际上对这个还好。 使用 TS 1.5 我将能够得到我想要的东西:

function isVoid(item:any): item is void { return item == null; }
declare externalUnsafeFunction(...):string|void

function test() {
  var res = externalUnsafeFunction(...);
  var words = res.split(' '); // error
  if (!isVoid(res)) {
    var words = res.split(' '); // ok
  }
}

现在,对externalUnsafeFunction或我将|void到返回类型的任何其他函数或函数定义的返回值强制执行isVoid检查。

我仍然无法声明不接受 null/undefined 的函数,但它可能足够勤奋以记住初始化局部变量和类成员。

因为@NoelAbrahams我们已经讨论过,即使使用 null 类型,仍然必须允许 null 隐式转换为任何其他类型,除非未来的编译器标志更改了它。

这也意味着将来我们可以引入一个编译器标志,让我们注释函数可以在何时何地接受 null。

当我们可以使用单词代替时,我个人讨厌使用符号来表示类型的想法。

@spion这实际上是一个好点。 如果目前在 TS 中有效,那么可以说所有内置的 d.ts 文件都已经“错误” ,添加您建议的空类型以及我建议的修改不会改变任何内容。

实际上,由于它已经在某种程度上是可以实现的,我将建议我们本周开始向我的老板使用它。

我要提醒的是,我们绝不会将T|void视为我们害怕将来破坏的语法。 这几乎是无稽之谈。

@RyanCavanaugh这和 void === undefined (一个可赋值的值)一样无意义。

叹。

我们的代码太大了,不能根据T|void如果它很快就会崩溃并且不会被替换。 如果任何一周它可能会中断,我将无法说服其他程序员使用它。

好的, @spion,如果您确实制作了补丁,请告诉我,我将根据我的工作代码库运行它。 我至少可以给出关于它导致多少错误的统计数据。

@RyanCavanaugh胡说八道,真的吗? 以什么方式? 您建议如何在空/未定义检查之前表达任何类型的消费都应该禁止的可为空类型?

我真的有很多图书馆可以从中受益。

@Griffork无论如何都无法在 1.5 之前安全地从联合中删除void ,而不是没有用户定义的类型保护,因此只能在 1.5 之后使用它(如果可能的话)

废话,你会写这段代码吗?

var foo: void;
var bar: void = doStuff();

当然不是。 那么将void到可能类型的联合中是什么意思呢? 请注意在描述The Void Type (3.2.4) 的部分中已经存在一段时间的语言规范的这一部分:

_注意:我们可能会考虑禁止声明 Void 类型的变量,因为它们没有任何用处。 但是,因为 Void 被允许作为泛型类型或函数的类型参数,所以禁止 Void 属性或参数是不可行的。_

这个问题:

_在空/未定义检查之前,您建议如何表达任何类型的消费都应该禁止的可为空类型?_

是整个线程的内容。 Ryan 只是指出string|void根据当前的语言语义是无意义的,因此依赖无意义的语义不一定合理。

我上面写的代码是完全有效的 TS 1.5,对吧?

我举个例子,这当然不是废话。 我们有一个(nodejs)数据库库函数,我们称之为get()类似于 Linq 的 Single() 不幸的是返回Promise<null>而不是在找不到项目时抛出错误(被拒绝的承诺)。 它在我们的代码库中的数千个地方使用,并且不太可能被替换或很快消失。 我想编写一个类型定义,强制我和其他开发人员在使用该值之前使用类型保护,因为我们已经有数十个难以追踪的错误,这些错误源于空值在被错误使用之前移动到代码库中很远。

interface Legacy { 
  get<T>(...):Promise<T|void>
}

function isVoid(val: any): val is void { return val == null; } // also captures undefined

legacy.get(...).then(val => {
  // val.consumeNormally() is a type error here
  if (!isVoid(val)) { 
    val.consumeNormally(); // OK
  }
  else { handle null case }
});

这在我看来完全合理。 将void到联合会导致val上的所有操作都无效,因为它们应该是无效的。 打字员通过删除|void并保留T部分来缩小类型。

也许规范没有考虑到带有 void 的工会的影响?

不要禁止 void 变量! 它们作为单位类型值工作,可以使用
在表达式中(与 C# 中的 void 不同,它需要 2 组所有内容:一个
用于操作和一个功能)。 请留空吧,好吗?
2015 年 1 月 27 日晚上 8:54,“Gorgi Kosev”通知@github.com 写道:

我上面写的代码是完全有效的 TS 1.5,对吧?

我给你举个例子,这当然不是废话。 我们有一个
不幸的是,类似于 Linq 的 Single() 的数据库库函数
回报承诺而不是抛出错误(被拒绝的承诺),当
未找到该项目。 它在我们的代码库中以数千种方式使用
并且不太可能被替换或很快消失。 我想写一个
强制我和其他开发人员使用类型保护的类型定义
在使用价值之前,因为我们有几十个难以追踪的错误
源于一个空值在被移入代码库之前
错误地消耗。

接口遗留{
得到(...):承诺
}
function isVoid(val: any): val is void { return val == null; } // 也捕获未定义

legacy.get(...).then(val => {
// val.consumeNormally() 是一个错误
如果 (!isVoid(val)) {
val.consumeNormally(); // 行
}
else { 处理空情况}
});

这在我看来完全合理。 你能指出哪个部分是
废话?

直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -71767358
.

我要说的是T|void现在基本上没有意义,因为我们已经有了这样的规则,即类型与其子类型的联合等同于简单的超类型,例如Cat|Animal等同于Animal 。 将void视为null / undefined的替代值是不连贯的,因为nullundefined _already 在所有TT _ 域,这意味着它们应该是T子类型

换句话说, T|null|undefined ,如果你能写出来,就已经可以折叠成T 。 将void视为null|undefined的咒语是错误的,因为如果确实如此,编译器就会将T|void折叠为T

我会亲自阅读这个http://www.typescriptlang.org/Content/TypeScript%20Language%20Specification.pdf

Void 类型的唯一可能值是 null 和 undefined。 Void 类型是 Any 类型的子类型,也是 Null 和 Undefined 类型的超类型,但除此之外,Void 与所有其他类型都无关。

作为; 在T|void , T 特别_不是_ void的超类型,因为唯一的超类型(根据规范)是any

编辑:禁止它是一个不同的问题。

编辑 2:重新阅读您所说的内容,从字面上看是有道理的(就像文档的措辞无效一样),但在假定文档的通用(或正确)解释中没有意义。

@RyanCavanaugh 无论如何我都会使用该功能,直到它

而且我不禁指出将T|null|undefined合并为T “有意义”而T|void的存在则没有的讽刺意味。 (我知道,它是关于规格的,但仍然......)

@jbondc

ECMAScript 中没有静态结构类型,因此该参数无效。 在 ECMAScript 中,所有值都是any因此任何东西都可以分配给任何东西或传递给任何东西。 这一事实不应该暗示 TypeScript 的静态类型系统有任何意义,它可以改进 JS 中不存在的类型系统

我不得不解释讽刺意味有点难过,但这里是:

  1. 你有一个不支持方法和属性的值, null
  2. 您创建了一个类型系统,其中该值可分配给任何类型的变量,其中大多数支持各种方法和属性(例如字符串数字或复杂对象var s:string = null
  3. 您创建了一个void类型,该类型正确接受该值并且不允许任何方法或属性,因此T|void不允许任何方法和属性。

声称(3)是无稽之谈,而(2)不是。 你看到这里具有讽刺意味的部分了吗? 如果没有,我将尝试另一个示例:

  1. 您的值仅支持某些方法和属性{}
  2. 您创建了一个类型系统,其中该值可分配给任何类型的变量(例如字符串数字或复杂对象var s:string = {} ),这些变量具有更多的方法和属性
  3. 你有一个{}类型,它正确地接受{}值,除了一些内置的方法或属性外,不允许任何方法或属性,而T|{}自然不允许除{}内置

现在哪个是废话,(2)还是(3)?

这两个示例之间的唯一区别是什么? 一些内置方法。

最后,接口只是一个类型定义描述。 它不是在任何地方实现的接口。 它是库提供的接口,它从不返回承诺以外的任何内容,但可能会返回 null 的承诺。 因此安全性绝对是非常真实的。

@spion这正是我的想法。

在这里,我认为这会让事情变得更清楚一点:
根据规范


什么是空?

void 类型,由 void 关键字引用,表示没有值,用作没有返回值的函数的返回类型。

所以,void 不是 null|undefined。

Void 类型的唯一可能值是 null 和 undefined

因此, null 和 undefined 可以分配给 void (因为它们几乎可以分配给其他任何东西)。


空是一种类型。

3.2.5 空类型
Null 类型对应于类似命名的 JavaScript 原始类型,并且是 null 的类型
文字。
null 字面量引用 Null 类型的唯一值。 无法直接引用 Null 类型本身。


未定义是一种类型。

3.2.6 未定义类型
Undefined 类型对应于类似命名的 JavaScript 原始类型,并且是未定义文字的类型。


Void 只是 Null 和 Undefined _types_ 的超类型

. Void 类型是 Any 类型的子类型,也是 Null 和 Undefined 类型的超类型,但除此之外,Void 与所有其他类型都无关。


所以@jbondc根据 Typescript Language Specification,null 是一个值,Null 是一个类型,undefined 是一个值,Undefined 是一个类型,而 Void(在代码中是小写的)是一个类型,它是 Null 的超类型和未定义,但 _does 不会隐式地将_转换为任何其他类型(除了any )。 事实上,Void 被专门编写为具有与 Null 和 Undefined 类型不同的行为:

空白:

Void 类型是 Any 类型的子类型,也是 Null 和 Undefined 类型的超类型,但除此之外,Void 与所有其他类型都无关。

空值:

Null 类型是所有类型的子类型,Undefined 类型除外。 这意味着 null 被视为所有基本类型、对象类型、联合类型和类型参数的有效值,甚至包括 Number 和 Boolean 基本类型。

不明确的:

未定义类型是所有类型的子类型。 这意味着 undefined 被视为所有原始类型、对象类型、联合类型和类型参数的有效值。

现在清楚了吗?

我们要求的是一种强制对空值进行保护的类型,以及一种强制对未定义进行保护的类型。 我不是要求 null 或 undefined 不能分配给其他类型(这太破坏了),只是能够选择加入额外的标记。 哎呀,它们甚至不必被称为 Null 或 Undefined(类型)。

@jbondc这就是讽刺:不管 TS 编译器怎么说,JS 中的 null 和 undefined 值永远不会支持任何方法或属性(除了抛出异常)。 因此,让它们可以分配给任何类型都是无稽之谈,就像允许将值{}分配给字符串一样无意义。 TS 中的 void 类型不包含任何值,_but_ nullundefined可以分配给每个人,因此可以分配给 void 类型的变量,所以这里还有一点废话。 这就是讽刺 - (通过类型保护和联合)目前都结合成一些真正有意义的东西:)

像 TS 编译器这样复杂的东西是在没有保护的情况下编写的,这是需要考虑的。

有相当大的应用程序是用 PHP 或 JAVA 编写的,这不会使语言变得更好或更糟。
埃及金字塔是在没有任何现代机器的情况下建造的,这是否意味着我们在过去 4500 年中发明的一切都是废话?

@jbondc请参阅我上面问题列表(减去列表中的第一个)

@jbondc它不应该解决世界上的问题,它应该是开发人员可以使用的另一种工具。
它与函数重载、联合和类型别名具有相同的影响和效用。
这是另一种选择加入的语法,允许开发人员更准确地键入值。

提到的使类型不可为空的一大问题是如何处理现有的 TypeScript 代码。 这里有两个问题:1) 使现有代码与支持不可为空类型的较新编译器一起工作 2) 与尚未更新的代码互操作 - 避免 Python 2/3 风格的泥潭

不过应该有可能提供一个工具来自动重写现有的 TS 代码以使其构建,并且对于这种有限的情况,这比 2to3 对 Python 所做的要好得多。

也许渐进式迁移看起来像这样:

  1. 引入对“?T”语法的支持以指示可能的空类型。 最初这对类型检查没有影响,它只是用于文档。
  2. 提供自动重写现有代码以使用“?T”的工具。 最愚蠢的实现会将所有使用的 'T' 转换为 '?T'。 更智能的实现将检查使用该类型的代码是否隐式假定它为非空并在这种情况下使用“T”。 对于尝试将类型为 '?T' 的值分配给期望为 'T' 的参数或属性的代码,这可以将值包装在一个占位符中,该占位符断言(在编译时)该值是非空的 - 例如let foo: T = expect(possiblyNullFoo)let foo:T = possiblyNullFoo!
  3. 将该工具应用于所有提供的类型定义以及绝对类型存储库中的类型定义
  4. 尝试将“?T”分配给需要“T”的插槽时引入编译器警告。
  5. 让警告无处不在,并验证移植是否可管理,并查看如何接收更改
  6. 在编译器的一个主要新版本中,将 '?T' 分配给一个需要 'T' 的槽是一个错误。

也许还值得引入一些可以禁用特定模块的警告的东西,以简化尚未应用该工具的代码的使用。

在之前的评论中,我假设没有模式可以选择纯 'T' 的可空性以避免分裂语言,并且即使步骤 6 从未被证明可行,步骤 1-3 本身也会很有用。

我的随机想法:

  1. expect()部分可能很复杂,需要深思熟虑。 不仅有赋值,还有接口、泛型或参数类型……
  2. 我_think_ TS 没有任何警告,只有错误。 如果我是对的,我不确定他们会为该功能引入警告。
  3. 即使很多人升级他们的代码,企业可能会很慢,甚至在现有代码库很大的情况下可能永远不想这样做。 这使得该主要版本成为一个巨大的突破性变化,这是 TS 团队不想要的。

也就是说,我仍然认为报告空错误的“选择加入”标志是一个可以接受的解决方案。 问题是当标志关闭时,相同的代码/定义应该具有相同的行为(减去错误),我还没有时间考虑。

@jods4 - 对于expect()部分,我的想法是,只要有?T ,编译器就会看到T | undefined因此您可以重复使用处理联合类型的规则可能的。

我同意我不确定 TypeScript 的“国旗日”有多现实。 除了“选择加入”标志外,它还可以是最终成为选择退出标志的选择加入标志。 重要的是对什么是惯用风格有明确的方向。

与此讨论相关的其他内容 - https://github.com/rwaldron/tc39-notes/blob/master/es6/2015-01/JSExperimentalDirections.pdf讨论 Chrome 团队对使用类型信息(通过 TypeScript 样式指定)的调查annotations) 在编译器中优化 JS。 那里有一个关于可空性的悬而未决的问题 - 他们想要不可为空的类型,但也不确定可行性。

只是要回这里。 我很惊讶我们还没有真正解决这个问题。

我说这里有一个附加值。 一种仅适用于编译时的。
不必编写更多代码来对值进行空检查,能够将值声明为不可空可以节省时间和编码。 如果我将一个类型描述为“不可为空”,那么在传递给另一个期望非空的函数之前,它必须被初始化并可能被断言为非空。

正如我上面所说,也许这可以简单地通过代码契约实现来解决。

为什么你不能停止使用 null 文字并保护所有地方
空值会泄漏到您的代码中吗? 这样你就可以避免空值
引用异常,而不必到处检查 null。
2015 年 2 月 20 日下午 1:41,“electricessence” [email protected]写道:

只是要回这里。 我很惊讶我们还没有真正解决
这个。

我说这里有一个附加值。 一种只适用于编译
时间。
不必编写更多代码来对值进行空检查,而是能够
将值声明为不可为空可以节省时间和编码。 如果我描述一个
类型为“不可为空”,那么它必须被初始化并可能被断言
在传递给另一个期望的函数之前作为非空
非空。

正如我上面所说,也许这可以简单地通过代码契约来解决
执行。

直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -75295204
.

空检查比未定义检查快得多。
在 21/02/2015 上午 11:54,“Aleksey Bykov”通知@ github.com 写道:

为什么你不能停止使用 null 文字并保护所有地方
空值会泄漏到您的代码中吗? 这样你就可以避免空值
引用异常,而不必到处检查 null。
2015年2月20日下午1:41,“electricessence”通知@ github.com
写道:

只是要回这里。 我很惊讶我们还没有真正解决
这个。

我说这里有一个附加值。 一种只适用于编译
时间。
不必编写更多代码来对值进行空检查,而是能够
将值声明为不可为空可以节省时间和编码。 如果我描述一个
类型为“不可为空”,那么它必须被初始化并且可能
断言
在传递给另一个期望的函数之前作为非空
非空。

正如我上面所说,也许这可以简单地通过代码契约来解决
执行。

直接回复此邮件或在 GitHub 上查看
<
https://github.com/Microsoft/TypeScript/issues/185#issuecomment-75295204>
.


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -75346198
.

我的观点是这些检查是不必要的(如果 null 关键字被禁止
并且所有其他可以进入的地方都受到保护)。

为什么? 1. 完全不检查的代码运行速度明显快于代码
检查空值。 2.它包含更少的if,因此更具可读性
并且可维护。
2015 年 2 月 20 日下午 7:57,“Griffork”通知@github.com 写道:

空检查比未定义检查快得多。
在 21/02/2015 上午 11:54,“Aleksey Bykov”通知@ github.com 写道:

为什么你不能停止使用 null 文字并保护所有地方
空值会泄漏到您的代码中吗? 这样你就可以避免空值
引用异常,而不必到处检查 null。
2015年2月20日下午1:41,“electricessence”通知@ github.com
写道:

只是要回这里。 我很惊讶我们还没有真正解决
这个。

我说这里有一个附加值。 一种只适用于编译
时间。
不必编写更多代码来对值进行空检查,而是能够

将值声明为不可为空可以节省时间和编码。 如果我
描述一个
类型为“不可为空”,那么它必须被初始化并且可能
断言
在传递给另一个期望的函数之前作为非空
非空。

正如我上面所说,也许这可以简单地通过代码契约来解决
执行。

直接回复此邮件或在 GitHub 上查看
<
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -75295204

.

直接回复此邮件或在 GitHub 上查看
<
https://github.com/Microsoft/TypeScript/issues/185#issuecomment-75346198>
.

直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -75346390
.

你没有抓住重点,我们使用空值来避免未定义的检查。
由于我们的应用程序没有一个一致的状态,我们有,并且
通常必须让事物为 null / undefined 而不是对象
代表那个。
在 21/02/2015 下午 12:20,“Aleksey Bykov”通知@ github.com 写道:

我的观点是这些检查是不必要的(如果 null 关键字被禁止
并且所有其他可以进入的地方都受到保护)。

为什么? 1. 完全不检查的代码运行速度明显快于代码
检查空值。 2.它包含更少的if,因此更具可读性
并且可维护。
2015 年 2 月 20 日下午 7:57,“Griffork”通知@github.com 写道:

空检查比未定义检查快得多。
在 21/02/2015 上午 11:54,“Aleksey Bykov”通知@ github.com
写道:

为什么你不能停止使用 null 文字并保护所有地方
在哪里
空值会泄漏到您的代码中吗? 这样你就可以避免空值
引用异常,而不必到处检查 null。
2015年2月20日下午1:41,“electricessence”通知@ github.com
写道:

只是要回这里。 我很惊讶我们还没有
解决
这个。

我说这里有一个附加值。 一种仅适用于
编译
时间。
不必编写更多代码来检查一个值,而是
有能力的

将值声明为不可为空可以节省时间和编码。 如果我
描述一个
类型为“不可为空”,那么它必须被初始化并且可能
断言
在传递给另一个期望的函数之前作为非空
非空。

正如我上面所说,也许这可以简单地通过代码解决
合同
执行。

直接回复此邮件或在 GitHub 上查看
<

https://github.com/Microsoft/TypeScript/issues/185#issuecomment -75295204

.

直接回复此邮件或在 GitHub 上查看
<
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -75346198

.

直接回复此邮件或在 GitHub 上查看
<
https://github.com/Microsoft/TypeScript/issues/185#issuecomment-75346390>
.


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -75347832
.

你也错过了我的分数。 为了保持您的状态一致并能够表示缺失值,您不需要使用空值或未定义。 还有其他方法,特别是现在有联合类型支持考虑:

class Nothing { public 'i am nothing': Nothing; }
class Something { 
    constructor(
    public name: string,
    public value: number
   ) { }
}
var nothing = new Nothing();
var whoami = Math.random() > 0.5 ? new Something('meh', 66) : nothing;

if (whoami instanceof Nothing) {
    console.log('i am a null killer');
} else if (whoami instanceof Something) {
    console.log(whoami.name + ': ' + whoami.value);
}

所以我们只是在不使用 null 或 undefined 的情况下编码了一个缺失值。 另请注意,我们是以100% 明确的方式进行的。 多亏了类型保护,在读取值之前不可能错过检查。

多么酷啊?

你认为检查的实例比空检查更快吗?
每秒必须执行数百个这样的应用程序?
让我这样说吧:我们不得不为函数创建别名,因为
不使用 '.' 会更快存取器。
在 21/02/2015 下午 12:58,“Aleksey Bykov”通知@ github.com 写道:

你也错过了我的分数。 为了保持您的状态一致和
能够表示您不需要使用空值或
不明确的。 还有其他方法,尤其是现在有联合类型支持
考虑:

class Nothing { public '我什么都不是':Nothing; }
类东西{
构造函数(
公共名称:字符串,
公共价值:数字
) { }
}
var nothing = new Nothing();
var whoami = Math.random() > 0.5 ? 新东西('meh',66):没有;

如果(whoami instanceof Nothing){
// whoami 什么都不是
console.log('我是一个空杀手');
} else if (whoami instanceof something) {
// whoami 是Something的一个实例
console.log(whoami.name + ':' + whoami.value);
}

所以只是在不使用 null 或 undefined 的情况下编码了一个缺失值。
另请注意,我们正在以_100% 明确的方式_进行操作。 感谢打字
守卫,在读取值之前不可能错过检查。

多么酷啊?


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -75349967
.

@aleksey-bykov 请注意,我已经解决了这一点:尚未解决为已经使用空值的现有库建模类型定义的需要。 在这种情况下,您不能只是“停止使用”空值。

此外,这甚至没有涉及undefined未初始化变量的问题。 在 Haskell 中,这个问题甚至不存在,因为你不能让一个值“未初始化”。

@Griffork ,我不认为,我运行测试,测试表明这取决于浏览器。
http://jsperf.com/nullable-vs-null-vs-undefined-vs-instanceof
但我认为您需要在安全性和性能之间找到平衡。 然后,您需要在尝试优化性能之前仔细衡量您的性能。 很有可能你正在修复一些没有损坏的东西,你非常关心的空检查可能只占整体性能的不到 2%,如果是这样,如果你让它快一倍,你只会获得 1%。

@spion ,考虑到当前情况和非空值带来的问题,所有事情都认为这是比在代码中继续使用空值更好的方法

我们的代码库有超过 800 个 TypeScript 文件和超过 120000 行代码
在建模业务时,我们从不需要 null 或 undefined
域实体。 虽然我们不得不使用空值来进行 DOM 操作,
所有这些地方都被仔细隔离,这样空值就没有办法泄漏
在。我不相信你的论点,即可能需要空值,有
完全没有空值(Haskell)或空值的生产就绪语言
禁止 (F#)。
2015 年 2 月 22 日上午 9:31,“Jon”通知@github.com写道:

@aleksey-bykov https://github.com/aleksey-bykov从实用
透视,你不能忽略空值。 这里有一些关于为什么你会的背景
无论如何都需要一个可为空的类型:
https://www.google.com/patents/US7627594?ei=P4XoVPCaEIzjsATm4YGoBQ&ved=0CFsQ6AEwCTge

直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -75437927
.

是的,您在这里可以理解的最重要的事情是我们没有对业务领域实体进行建模,我们对时间非常敏感,而且我们的代码库不会比您的代码小太多。

为了您的目的,您不需要空值,这很好,您可以继续使用没有它们的 Typescript。
为了我们的目的,我们需要空值,我们不能不这样做。

哦,刚看到你的另一条评论,抱歉。
如果您稍早阅读本主题中的内容,您会注意到我已经说过(基本上)我们一直使用空值,并且我们很少遇到它们渗入其他代码位的问题。

如果您还阅读了上面的内容,则不可为空的提案也涵盖了未定义的内容。

Null 和 undefined 在变量中的速度看起来很相似,但是一旦它们出现在一个对象上(特别是一个带有原型链的对象),情况就大不相同了,所以我们使用 null 来代替。

同样,可能不值得担心,除非(像我们一样)在 <10 毫秒内进行 10,000 次检查。

(是的,由于跟踪应用程序的性能并发现瓶颈,我们实际上发生了变化)

我想激发一些关于这个的讨论。 就在昨天的 Build 视频中,我看到了用 C# 实现的分析器。 我打开了一个问题 #3003 来实现类似于 TypeScript。

基本上分析器是一个库,可以进行额外的语法检查,将行标记为错误/警告,并由语言服务解释。 这将有效地允许使用外部库强制执行空检查。

事实上,我看到 TypeScript 中的分析器对于其他事情也非常有用。 那里有很多奇怪的库,比 C# 还多。

对于关注此问题的任何人:我正在维护一个名为monapt的库,它可以让您声明变量的可空性。 它包括 Typescript 定义,因此所有内容都经过编译时检查。

欢迎反馈!

我只是通读了整个线程。

我想提案应该使用non-voidablevoidable条款,因为 TS 规范中的voidnullundefined值的类型. (我假设该提案还包括未定义的值:微笑:)

我也同意访问未定义或空异常是编程中万恶的根源,添加此功能将为人们节省无数时间。

请添加这个! 如果这成为一项功能,我可以传递一个标志以使其成为默认值,那将是最好的。 这将为很多人节省大量的调试地狱。

虽然我同意 ADT 可以用来代替可空类型; 我们如何确定我们传递的 ADT 本身不是空的? TS 中的任何类型都可以为空,包括 ADT,对吗? 我认为这是将 ADT 视为解决不可空(不可空)类型问题的一个主要缺陷。 ADT 只能像在允许(或禁止)不可为空类型的语言中一样有效。

另一个关键问题是库定义。 我可能有一个需要字符串作为参数的库,但是 TS 编译器可以通过类型检查将null传递给函数! 那是不对的...

我认为这是将 ADT 视为问题解决方案的一个主要缺陷......

  1. 通过设计模式获取 ADT 可用(由于缺少本机
    来自 TypeScript 的支持)
  2. 禁止null关键字
  3. 保护所有空值可能从外部泄漏到您的代码的地方
  4. 享受null问题一去不复返了

在我们的项目中,我们设法完成了第 1、2 和 3 步。猜猜我们在做什么
现在。

@aleksey-bykov

你如何用undefined做(2)? 它可能出现在不涉及undefined关键字的各种情况下:未初始化的类成员、可选字段、条件分支中缺少 return 语句......

比较打字稿流动型。 至少流程可以处理大多数情况。

此外,如果您愿意说“忘记语言中的常见习语。我不在乎,我会让自己与世界其他地方隔离”,那么您最好使用purescript

它可能出现在不涉及 undefined 关键字的各种情况下......

  • 未初始化的类成员 - 禁止类
  • 可选字段 - 禁止可选字段/参数
  • 缺少返回语句 - 确保所有执行路径返回的自定义 tslint 规则

最好使用 purescript

  • 我希望但它生成的代码最不关心性能

@aleksey-bykov 有趣的是,您提到了性能,因为与仅强制执行空检查(如 flowtype)相比,用 ADT 替换可空类型会带来显着的 CPU 和内存开销。 不使用类也是如此(如果你用很多方法实例化很多对象,可能会非常昂贵)

你提到放弃可选字段,但它们被很多 JS 库使用。 你做了什么来处理这些库?

您还提到不使用类。 我想您还避免依赖(或将依赖)类的库(例如 React)?

无论如何,当一个完全合理的解决方案(实际上适合底层无类型语言)最有可能实现时,我认为没有理由放弃这么多功能。

有趣的是,您认为 ADT 的唯一目的是表示一个缺失的值,否则该值将被 null 编码

我宁愿说“空”问题消失了(还有类)是 ADT 带给我们的所有好东西的副产品

解决你的好奇心,对于像紧密循环和大型数据结构这样的性能关键部分,我们使用所谓的Nullable<a>抽象,通过欺骗 TypeScript 认为它处理包装的值,但实际上(在运行时)这样的值只是一个允许采用空值的原语,在 Nullable 上有一组特殊的操作可以防止该空值泄漏

如果你想知道细节,请告诉我

interface Nullable<a> {
    'a nullable': Nullable<a>;
    'uses a': a;
}
/*  the following `toNullable` function is just for illustration, we don't use it in our code,
    because there are no values capable of holding naked null roaming around,
    instead we just alter the definition of all unsafe external interfaces:
    // before
    interface Array<T> {
       find(callback: (value: T, index: number, array: T[]) => boolean, thisArg?: any): T;
    }
    // after
    interface Array<a> {
       find(callback: (value: a, index: number, array: a[]) => boolean, thisArg: any): Nullable<a>;
    }
*/
function toNullable<a>(value: a) : Nullable<a> {
    return <any>value;
}
function toValueOrDefault<a>(value: Nullable<a>, defaultValue: a)  : a {
    return value != null ? <any>value : defaultValue;
}

为什么使用递归类型结构? 'a nullable': a应该足够了,不是吗?

我想你还有callFunctionWithNullableResult<T,U>(f: (T) => U, arg:T):Nullable<U> (包括所有其他具有不同数量的函数的重载)来处理所有返回空值的外部函数?

如果 linter 禁止使用 null 关键字,那么如何将“ Nothing ”存储在可空对象中? 你有特殊情况toNullable(null)吗?

我知道什么是代数数据类型。 你能给我一个实例(除了穷举检查和笨拙),其中 ADT(或者更确切地说,和类型)+模式匹配可以做一些打字稿的联合+用户定义的类型保护不能做的事情?

为什么使用递归类型结构? 'a nullable':a 应该足够了,不是吗?

好吧,在这个例子中就足够了,但是这样做会重载此类字段的目的,它有两种不同的情况:

  • 将类型转换为名义(您示例中的文件名): https :
  • 使其了解泛型类型参数,以便将其考虑到对象的表面(示例中的值类型): https :

我个人讨厌超载,这就是为什么我使用 2 个单独的伪字段,每个字段都解决一个单独的问题

我想你也有 callFunctionWithNullableResult(f: T -> U, arg:T) => 可空

不,正如我所说,我们通过用Nullable “包装器”替换外部代码中我们有证据可以为空的任何内容来更改定义(*.d.ts)文件,请参阅注释中的示例在我之前的消息中

你如何得到一个“Nothing”存储在可空值中

我们不在可空值中存储空值,可空值来自服务器(我们对其控制有限)或来自外部 API,一旦它在我们的代码中,它就会被包裹在 Nullable 中,但我们的代码无法生成一个可空的随意,我们只能将一个给定的可空转换为另一个可空,但不能凭空创造一个

打字稿的联合 + 用户定义的类型保护不能做

哈! 我根本不喜欢工会类型,并且可以谈论几个小时使他们成为坏主意的原因

回到你的问题,给我一个可重用的Either a b使用联合和类型保护
(提示:https://github.com/Microsoft/TypeScript/issues/2264)

您示例的类型并

不过要回答您的问题:您没有实现通用的任何一种,正是因为类型不是名义上的。 您只需使用

type MyType = A | B

然后对AB使用类型保护......作为奖励,这也适用于每个现有的 JS 库:例如,检查Thenable的惯用方法是是附加到对象的then方法。 类型保护会为此工作吗? 最肯定的。

哈! 这是你的一个该死的好渔获物。 好吧,在最近版本的编译器中一定发生了一些变化。 这是防弹标称:

enum GottaBeNonimalBrand {}
interface GottaBeNonimal {
     branded: GottaBeNonimalBrand;
}
function toGottaBeNominal() : GottaBeNominal {
   return <any> {};
}

等一等! 你被骗了! 古老的方式仍然像魅力一样有效。

不过要回答您的问题:您没有实现通用的任何一种,正是因为类型不是名义上的。 您只需使用...

你说的真好,不过Either怎么样? 那个正宗的Either通用的,它周围有大约 30 个标准的便利函数,现在你说我需要解决(名义上的)并且有多达 30 x number_of_combinations_of_any_2_types_in_my_app 的这些函数的所有可能的对我可能遇到?

希望此线程仍由 TS 团队的人员监控。 我想尝试重新启动它!
在我看来,当很明显不可能以不间断的方式引入非空类型时,非空类型就卡住了。 C# 团队正在考虑不可为空的引用类型,当然也面临着同样的问题,尤其是兼容性问题。 我喜欢他们目前正在探索的想法,我想知道它们是否适用于 TS。

C# vnext 头脑风暴(简要)

基本上,在 C# 中,您有非空值类型,例如int 。 然后在 C# 2.0 中我们得到了可以为 null 的值类型,例如int? 。 引用一直可以为空,但他们正在考虑改变这一点并应用相同的语法: string永远不会为空,而string?可能是。

当然,最大的问题是这改变了string的含义并破坏了代码。 可以将null分配/返回给string ,现在是一个错误。 C# 团队正在考虑“禁用”这些错误,除非您选择加入。 加点到string?总是一个错误,因为语法是新的,以前是不允许的(因此这不是一个重大变化)。
另一个问题是其他库等。为了解决这个问题,他们希望为每个文件或程序集设置选择加入标志。 因此,选择使用更严格的类型系统的外部代码会获得好处,并且旧的遗留代码和库可以继续工作。

这怎么能转换成TS?

  1. 我们引入了一个非空类型的选择加入标志。 它可以是全局的(传递给编译器)或每个文件。 .d.ts应该始终是每个文件。
  2. 基本类型不可为空,例如number 。 有一个新的null类型。
  3. number?只是number | null的快捷方式。
  4. 该功能为可空类型引入了新错误,例如没有类型保护的(x: string?) => x.length
  5. 该功能为不可为空类型引入了新错误,例如assign let x: string = null;
  6. 操作在选择加入范围之外声明的非空类型时,5. 下报告的错误将被忽略。 _这与说string仍被视为string?略有不同。_

这有什么好处?

当我编写新代码时,我可以选择加入并在我的代码和更新的库定义中进行完整的静态空检查。 我可以继续使用旧库定义而不会出错。

当我使用旧代码时,我可以选择加入新文件。 无论如何,我可以声明string?并且在没有正确检查空值的情况下访问成员时会出错。 对于定义已更新的库,我还可以获得好处和严格检查。 一旦.d.ts选择加入,将null传递给定义为length(x: string): number的函数就会出错。
_(注意:传递string?也是一个错误,尽管从选择退出代码传递string不是。)_

只要我不选择加入,就没有重大变化。

你怎么认为?

当然,关于该功能在实践中如何工作,需要考虑很多。 但是,TS 团队甚至可能会考虑这样的选择加入方案吗?

人们想要更快的马而不是汽车,真是太神奇了。

没必要居高临下。 我已经阅读了您对 ADT 的评论,我认为它们不是我所需要的。 如果您愿意,我们可能会讨论这个问题,但会在有关“TS 中的 ADT 支持”的新问题中讨论。 您可以在那里写下为什么它们很棒,为什么我们需要它们以及它们如何减轻对不可空类型的需求。 这个问题是关于不可为空的类型,你真的在​​劫持这个话题。

您所谈论的称为常量传播,如果实现,不应仅受 2 个随机常量的限制,这些常量恰好是nullundefined 。 对字符串、数字和其他任何东西都可以这样做,这样就有意义了,否则只是为了培养一些人不愿意完全养成的旧习惯

@aleksey-bykov 当然,为了获得更好的类型限制,您可以实现更高种类的类型,并获得派生实例,您可以实现类型类等等。 这如何符合 TypeScript 的“与 ES6+ 一致”的设计目标?

从这个问题解决原始问题仍然没有用。 即使在今天(没有联合类型),它也很容易使用泛型类在 TS 中实现 Maybe(和Either)。 它将与向 Java 添加Optional一样有用,即:几乎没有。 因为有人会在某处分配null或需要可选属性,而语言本身不会抱怨,但事情会在运行时爆炸。

我不清楚你为什么坚持滥用语言来让它以它从未设计过的方式弯曲。 如果您想要一种具有代数数据类型且没有空值的函数式语言,只需使用 PureScript。 它是一种优秀的、设计良好的语言,具有连贯一致的特性,多年来在 Haskell 中进行了实战测试,没有 Haskell 积累的缺陷(有原则的类型类层次结构,具有行多态性的真实记录......)。 在我看来,从一种符合您的原则和需要的语言开始,然后调整性能要容易得多,而不是从没有(并且从来没有打算)的东西开始,然后扭曲,转向和限制以使其达到以您的方式工作。

PureScript 的性能问题是什么? 是普遍的咖喱产生的闭包吗?

@jods4是否意味着所有当前存在的 .d.ts 文件都需要使用停止工作的标志进行更新,或者项目范围的设置不会影响 .d.ts(和 .d.ts没有标志总是被认为是“旧”的方式。
这有多容易理解(是否会给从事多个项目的人带来问题或在阅读 .d.ts 文件时造成混淆)?
新旧(库)代码之间的接口是什么样的。 它是否会充斥着一堆(在某些情况下)不必要的检查和强制转换(可能不是一件坏事,但它可能会让初学者远离这门语言)?
大多数情况下,我(现在)喜欢不可为空类型的想法, @jods4的建议和!建议是我看到不可为空类型工作的唯一方式(忽略 adts)。

它可能在这个线程中丢失了,但这解决了什么问题? 这是暗示我的事情。 我最近在这个线程中看到的论点是“其他语言也有”。 我也很难看到这解决了什么有用的问题,不需要对 TypeScript 进行某种诡计。 虽然我可以理解nullundefined值可能是不受欢迎的,但我倾向于将其视为我想放入代码中的高阶逻辑,而不是期望底层语言来处理它.

另一个小问题是,每个人都将undefined视为常量或关键字。 从技术上讲也不是。 在全局上下文中,有一个变量/属性undefined是原始类型undefinednull是一个文字(它也恰好是原始类型null ,尽管它是一个长期存在的错误,它被实现为返回typeof "object" )。 在 ECMAScript 中,这是完全不同的两件事。

@kitsonk
问题是所有类型都可以为空,并且所有类型都可以是未定义的,但并非所有适用于特定类型的操作都不适用于空值或未定义的值(例如除法),从而导致错误。
他们想要的是能够将类型指定为“是数字并且永远不会为空或未定义”,以便他们可以确保在使用可能引入空或未定义的函数时,程序员知道要防止无效值。
能够将值指定为“是这种类型但不能为空或未定义”有两个主要参数是:
A) 对于所有使用代码的程序员来说,很明显这个值永远不能为空或未定义。
B) 编译器帮助捕获丢失的类型保护。

有一个未定义的变量/属性是未定义的原始类型。 null 是一个文字(它也恰好是原始类型 null

@kitsonk这是这个问题的一个很好的论据; null不是number ,它不再是Person不是Car 。 它是它自己的类型,这个问题是关于允许打字稿做出这种区分。

@kitsonk我认为最常见的错误之一是空引用/访问未定义错误。 具有将属性或变量指定为非空/未定义的能力将使软件代码更合理。 并且还让诸如 LS 之类的工具来捕获这样的错误https://github.com/Microsoft/TypeScript/issues/3692。

感谢您的澄清。 也许我在这里也错过了一些东西,回顾了这个......既然在 TypeScript 中已经有一个“可选性”参数,为什么以下不能作为建议?

interface Mixed {
    optional?: string;
    notOptional: string;
    nonNullable!: string;
}

function mixed(notOptional: string, notNullable!: string, optional?: string): void {}

虽然在一些提议的解决方案中有更强大的功能,但我怀疑对它们进行类型保护成为一个挑战,因为这(虽然我不太了解 TypeScript 的内部工作原理)似乎是已经被检查的内容的扩展。

既然 TypeScript 中已经有一个“可选性”参数,为什么以下不能作为建议?

只是为了澄清现在 TS 中的选项仅在初始化期间是可选的。 Nullundefined仍然可以分配给所有类型。

这些是一些不直观的选项案例

interface A {
   a?: string;
}
let a: A = {} // ok as expected

interface B {
   b: string;
}
let b1: B = { b: undefined } //ok, but unintuitive
let b2: B = { b: null } // ok, but unintuitive
let b3: B = {} // error as expected

或者在 TS 中保持简单optional意味着不可初始化。 但是这个提议说 undefined 和 null 不能分配给一个 non-voidable(non-null, non-undefined) 类型。

?由属性/参数名称放置,因为它指代该名称的 _existence_ 与否。 @tinganho解释得很好。 扩展他的例子:

function foo(arg: string, another?: string) {
  return arguments.length;
}

var a: A = {};
a.hasOwnProperty('a') // false
foo('yo') // 1, because the second argument is _non-existent_.
foo(null) // this is also ok, but unintuitive

非空的!与名称的 _existence_ 无关。 它与类型有关,这就是为什么它应该按类型放置。 该类型不可为空。

interface C {
  c: !string;
}

let c1: C = { }; // error, as expected
let c2: C = { c: null }; // error, finally!
let c3: C = { c: 'str' }; // ok! :)

function bar(arg: !string) {
  return arg.length;
}

bar(null) // type error
bar('foo') // 3

@Griffork现有的.d.ts可以正常工作,并且可以顺利更新以支持更严格的定义。
默认情况下, .d.ts不选择加入。 如果库作者使用忠实的非空定义更新其库,它会通过在文件顶部设置一个标志来表明这一点。
在这两种情况下,一切都可以正常工作,而无需消费者进行任何思考/配置。

@kitsonk这解决的问题是减少错误,因为人们认为某些东西可能不是空的,或者将空值传递给不支持它的函数。 如果您与许多人一起处理大型项目,您可能会使用不是您编写的函数。 或者您可能会使用您不太了解的 3rd 方库。 当你这样做时: let x = array.sum(x => x.value) ,你能假设x永远不会为空吗? 如果数组为空,也许它是 0? 你怎么知道? 你的收藏允许有x.value === null吗? 这是sum()函数支持的还是会出错?
使用此功能,静态检查这些情况并将可能的空值传递给不支持它的函数,或者在没有检查的情况下使用可能的空值将报告为错误。
基本上,在一个完全类型化的程序中,不再有“NullReferenceException”(不幸的是,减去与undefined相关的极端情况)。

关于可选参数的讨论是无关的。 是否允许 null 与类型系统有关,与变量/参数声明无关,它与类型保护、联合和交叉类型等很好地集成在一起。

类型 _void_ 没有任何值,但nullundefined值都可以分配给它

这是一个很好的总结: https :

@jbondc我想我最近检查了规范。 voidnullundefined总称。 不知道void 0 ,但我猜void也有它的保护伞。

参考: https :

推断any 。 但是nullundefined仍然可以分配给所有类型。

void 0总是返回undefinedhttp://stackoverflow.com/a/7452352/449132。

@jbondc如果问题是关于一个潜在的空感知类型系统,比如我上面描述的那个,

function returnsNull() {
  return null;
}

应该推断类型null

undefined周围有很多角落案例,老实说,我(还?)不确定解决它的最佳方法是什么。 但在花大量时间考虑之前,我想知道 TS 团队是否同意选择加入策略。

上次我参加这个讨论时,它的结尾是:“我们不会进行重大更改,我们不想根据环境标志/选项更改源代码的含义”。 我上面描述的想法不是一个重大变化,并且在源解释方面相对较好,尽管不是 100%。 那个灰色区域就是我问 TS 团队的想法的原因。

@jods4我更喜欢推断为可空的any ,它会保留现有的语法。 选择加入策略会更可取。 [1]

我更喜欢不可空类型的显式语法。 对于对象,它会同时破坏一些太多的东西。 而且,出于显而易见的原因,可选参数应该始终可以为空(并且应该不可能更改)。 默认参数应在外部保留其当前签名,但在函数本身内部,该参数可能不可为空。

不过,我确实觉得很荒谬,因为可空数字破坏了很多优化引擎可以做的事情,而且这不是可选/默认参数之外的常见用例。 通过编译器标志使不是可选参数的数字在默认情况下不可为空可能更可行/不中断。 这还需要一种语法来显式标记可为空类型,但我已经将其视为本次讨论的可能结果。

[1] 出于实际原因,首先引入一个标志使其成为默认值不会很好地工作。 看看人们在使用--noImplicitAny遇到的问题,这会导致一些迁移危险,测试属性存在时的许多实际问题导致--suppressImplicitAnyIndexErrors ,以及几个损坏的绝对类型定义。

@impinball

我更喜欢推断为可空的any ,它会保留现有的语法。 选择加入策略会更可取。

我在这里编辑了我的答案。 考虑更多这一点,我只是没有看到任何可以隐式推断null类型的有用情况。 如: () => nulllet x = nullfunction y() { return null }

所以我同意继续推断any可能是一件好事。

  1. 它向后兼容。
  2. 无论如何,所有这些情况都是noImplicitAny下的错误。
  3. 如果你有一个奇怪的情况,你想这样做并且它对你有用,你可以明确地声明类型: (): null => nulllet x: null = nullfunction y(): null { return null }

我更喜欢不可空类型的显式语法。 对于对象,它会同时破坏一些太多的东西。

string实际上意味着string! | null可能是另一种选择。 应该注意的是,如果您阅读了上面的所有讨论,那么最初提出这是因为希望它与现有代码更兼容(不改变string的当前含义)。 但事实上结论是,即便如此,大量代码无论如何都会被破坏。

鉴于采用不可为空的类型系统对于现有代码库来说不是一个微不足道的切换,我会选择string?选项而不是string!因为它感觉更干净。 特别是如果您查看 TS 编译器源代码,其中几乎所有内部​​类型的命名都将不准确:(

通过编译器标志使不是可选参数的数字在默认情况下不可为空可能更可行/不中断。 这还需要一种语法来显式标记可为空类型,但我已经将其视为本次讨论的可能结果。

我认为在这一点上事情变得令人困惑。 对我来说,这似乎是在任何地方使用string?的好理由。 我不希望看到混淆number?string! ,这会很快变得太混乱。

[1] 出于实际原因,首先引入一个标志使其成为默认值不会很好地工作。

是的,如果没有更改,它对于现有的代码库不会很好地工作。 但:

  1. 它对新的代码库很有用。 我们可以用它建立一个更美好的未来。
  2. 现有的代码库将获得_一些_好处和额外的检查,即使它们从不选择加入。
  3. 现有的代码库可以在每个文件的基础上选择加入,这允许新代码更加严格,如果需要,可以允许旧代码的渐进过渡。

@jods4

我认为在这一点上事情变得令人困惑。 对我来说,这似乎是使用字符串的一个很好的论据? 到处。 我不想看到混淆数字? 和字符串!,这会太快变得太混乱。

我不是那个意思。 我的意思是可空数字的用例比可空字符串的用例要少。 无论如何,我对那个建议并不那么执着(无论如何我都不在乎)。

现有的代码库可以在每个文件的基础上选择加入,这允许新代码更加严格,如果需要,可以允许旧代码的渐进过渡。

用什么手段? 我不相信您目前可以逐个文件地配置编译器。 我愿意在这里被证明是错误的。

@impinball

用什么手段? 我不相信您目前可以逐个文件地配置编译器。 我愿意在这里被证明是错误的。

也许我错了。 我从未使用过它们,但是在查看测试用例时,我看到很多看起来像// <strong i="9">@module</strong> amd 。 我一直认为这是一种从 ts 文件中指定选项的方法。 不过,我从未尝试过这样做,所以也许我完全错了! 在这里查找示例:
https://github.com/Microsoft/TypeScript/blob/master/tests/cases/conformance/externalModules/amdImportAsPrimaryExpression.ts

如果这不是指定每个文件选项的方法,那么我们可能需要一些新的东西。 需要为每个文件指定此选项,至少为.d.ts文件指定此选项。 文件顶部的特殊格式注释可能会起作用,毕竟我们已经支持/// <amd-dependency />和 co。

编辑:我错了。 深入研究源代码告诉我,这些东西被称为标记,并由 FourSlash 运行程序预先解析。 所以它仅用于测试,它不在编译器内部。 为此需要想出一些新的东西。

在某些情况下也存在实际限制 - 对于 ES6 功能等某些参数来说,这不是最实用的,因为它需要完全重新解析文件。 并且 IIUC 类型检查首先与 AST 创建在同一阶段完成。

如果您查看解析器代码,则会在解析文件中的任何其他内容之前读取/// <amd-dependency />/// <amd-module />注释。 添加类似/// <non-null-types />东西是可行的。 好吧,这是一个糟糕的命名,我不赞成任意“魔法”选项的扩散,但这只是一个想法。

@ jods4我觉得@I“M弹球是说的IDE不会没有大的变化的语法高亮是如何工作支持。

2.0 中会有任何形式的不可为空类型吗?
毕竟,typescript如果不能保证类型,给javascript带来的东西就少了。克服typescript引入的所有问题和重写后,我们能得到什么?
只是更好的智能感知? 由于您仍然需要在每个导出到其他模块的函数中手动或通过代码检查类型。

大家好消息,TypeScript 1.6 具有足够的表达能力来建模和安全地跟踪缺失值(否则由nullundefined值编码)。 本质上,以下模式为您提供了不可为空的类型:

declare module Nothing { export const enum Brand {} }
interface Nothing { 'a brand': Nothing.Brand }
export type Nullable<a> = a | Nothing;
var nothing : Nothing = null;
export function isNothing(value: Nullable<a>): value is Nothing {
    return value == null;
}
var something = Math.random() > 0.5 ? 'hey!' : nothing;
if (isNothing(something)) {
    // missing value
    // there is no way you can get anything out of it
    // there is also NO WAY to get a null reference exception out of it
    // because it doesn't have any methods or properties that could be examined
    // it is 100% explicit and typesafe to use
} else {
    // value is present, it is 100% GUARANTEED being NON-NULL
    // you just CANT get a null reference exception here either
    console.log(something.toLowerCase());
}

/** turns any unsafe values into safe ones */
export function sanitize<a>(unsafe: a) : Nullable<a> {
    return unsafe;
}

var safe = sanitize(toResultFromExternalCodeYouCannotTrust()); // <-- 100% safe to use

话虽如此,应该关闭请求,因为不再存在问题。 案件结案,下课。

@aleksey-bykov

话虽如此,应该关闭请求,因为不再存在问题。

你不会是认真的吧? 我们已经多次告诉您,可空代数类型的模式不是这个问题。

我_不_要将代码中的每一个var x: number包装成Nullable<number>甚至更好的NonNullable<x> 。 这只是太多的开销。 但是我想知道在我的代码中发生这种情况的任何地方,执行x *= 2都是 100% 安全的。

您的代码没有解决使用 3rd 方库的问题。 我_不_要在_every_ 3rd 方库上调用sanitize ,甚至我调用的内置 DOM api。 此外,我不想在整个地方添加isNothing(safe) _all_。 我想要的是能够调用let squares = [1,2,3].map(x => x*x)并且在编译时 100% 确定squares.length是安全的。 使用我使用的 _any_ API。

同样,我想要一个函数是否接受null作为输入的 3rd 方 JS 库的文档。 我想在编译时知道$(element).css(null)是正常还是错误。

我想在团队环境中工作,在那里我不能确保每个人都一致地使用像你这样的复杂模式。 您的Nulllable类型绝对不会阻止开发人员执行let x: number = null; x.toString() (或不那么愚蠢的事情,但效果相同)。

等等等等。 这张票离关闭还很远,问题仍然是 100%。

你不会是认真的吧?

我是很认真的。

我不会包装每一个......

为什么不? 使用!语法或你们推动的其他任何东西,无论如何你都必须这样做。

甚至更好的 NonNullable

它应该是Nullable我们正在建模的是可以为 null 的类型,它可以具有不可为 null 的值null并且它被明确说明。 与传统类型相反,传统类型总是有一个值或 null 并被称为number这意味着您应该在使用前检查它是否为 null。

这只是太多的开销。

在哪里?

您的代码没有解决使用 3rd 方库的问题。

确实如此。

我不会在我调用的每个 3rd 方库,甚至内置 DOM api 上调用 sanitize。

你不必。 您需要做的就是通过用Nullable<*>替换所有可以为 null 的内容来修复该 3rd 方库的定义文件

同样,我想要第 3 方 JS 库的文档,说明函数是否接受 null 作为输入。

一样。 将该方法定义为接受Nullable<string>而不是普通的string

您的 Nullable 类型绝对不会阻止开发人员执行 let x: number = null; x.toString()

确实没有。 您需要使用 linter 禁止null关键字。

来吧,我的解决方案 95% 有效,100% 实用,今天可用,无需做出艰难的决定,也无需让每个人都达成共识。 这是你追求的问题:一个看起来不像你期望但仍然有效的工作解决方案,或者在顶部有所有back jack和cherries的情况下完全按照你想要的方式获得它

在 1.4 和 1.5 中,void 类型不允许 Object 的任何成员,包括像 .toString(),因此type Nothing = void;应该足够而不需要模块(除非在 1.6 中再次更改)。 http://bit.ly/1OC5h8d

@aleksey-bykov 那不是真的。 您仍然必须小心清理所有可能为空的值。 如果您忘记这样做,您将不会收到警告。 它肯定是一种改进(需要注意的地方较少)。

另外,为了说明 hacks 不会真正让你走得很远,试试吧

if (isNothing(something)) {
  console.log(something.toString())
}

我不会包装每一个......

为什么不? 和 ! 语法或你们正在推动的任何其他事情,无论如何你都必须这样做。

好吧,如果其他一切都解决了,我_可以_这样做。 如果一切都解决了,也许 TS 甚至可以为此考虑一些语言糖。

甚至更好的 NonNullable

它应该是 Nullable 我们正在建模的是可以为 null 的类型,它可以具有不可为 null 的值或 null 并且它被明确说明。 与传统类型相反,传统类型总是有一个值或空值并被称为数字,这意味着您应该在使用前检查它是否为空。

并不是我只想摆脱空异常。 我也希望能够以静态安全方式表达不可为空的类型。
假设您有一个始终返回值的方法。 像toString()总是返回一个非空字符串。
你有哪些选择?
let x: string = a.toString();
这不好,因为没有静态验证x.length是安全的。 在这种情况下它是,但正如你所说的内置string可能是null所以这就是_status quo_。
let x = sanitize(a.toString());
好的,现在我不能在没有空检查的情况下使用它,所以代码 _is_ 安全。 但我不想在代码中使用x所有地方添加if (isNothing(x)) ! 它既丑陋又低效,因为我很清楚x在编译时不为空。 执行(<string>x).length效率更高,但在任何您想使用x地方都必须这样做仍然很丑陋。

我想做的是:

let x = a.toString(); // documented, non-null type string (string! if you want to)
x.length; // statically OK

如果没有适当的语言支持,您将无法实现这一点,因为 JS(和 TS 1.6)中的所有类型始终可以为 null。

我想说,尽可能多地使用非可选(可为空)类型进行编程是一种非常好的做法。 所以我在这里描述的是一个必不可少的场景,而不是一个例外。

这只是太多的开销。

在哪里?

看我之前的回答。

您的代码没有解决使用 3rd 方库的问题。

确实如此。

问题只解决了一半。 假设库定义按照您的建议更新:每个可为空的输入或返回值都被Nullable<X>而不是 X 替换。

我仍然可以将null传递给不接受null参数的函数。 好的,这个事实现在是 _documented_(我们已经可以用 JSDoc 注释或其他什么来做),但我希望它在编译时 _enforced_。
示例: declare find<T>(list: T[], predicate: (T) => bool) 。 两个参数都不应该为空(我没有使用Nullable )但我可以做find(null, null) 。 另一个可能的错误是声明说我应该从谓词返回一个非空的bool ,但我可以这样做: find([], () => null)

您的 Nullable 类型绝对不会阻止开发人员执行 let x: number = null; x.toString()

确实没有。 您需要使用 linter 禁止 null 关键字。

这样做并提供一个nothing全局变量将不会帮助您解决我在先例中列出的情况,是吗?

在我看来,这不是 95%。 我觉得许多常见事物的语法变得更糟,而且当我谈论不可空类型时,我仍然没有我想要的所有静态安全性。

您仍然必须小心清理所有可能为空的值。

就像你们在这里谈论的假设的不可空类型一样,你必须做同样的分离工作。 您必须查看所有定义文件并在适当的地方放置! 。 那有什么不同?

如果您忘记这样做,您将不会收到警告。

一样。 一样。

说明 hacks 不会真正让你走得很远,试试吧

虽然你正确的使用方式,我定义没什么其中有toStringObject ,但..如果我们把@Arnavion的想法(使用voidNothing ) 突然点击

手表

image

在我看来,这不是 95%。 我觉得许多常见事物的语法变得更糟,而且当我谈论不可空类型时,我仍然没有我想要的所有静态安全性。

伙计,你刚刚说服了我,这个模式不适合你,永远不会,请不要在你的代码中使用它,继续要求真正的不可空类型,祝你好运,抱歉打扰你这样的废话

@aleksey-bykov
我明白你在做什么,尽管在我看来你还没有解决所有的问题,而且当有人指出一个漏洞时,你似乎正在制定解决方案(比如替换null by void在最后一个例子中)。

最后_也许_你会按照我想要的方式工作。 您将为可空类型使用复杂的语法,您将使用 linter 禁止程序中null任何来源,这可能最终使可空类型成为非空类型。 在此过程中,您必须说服每个人使用您的非标准约定更新他们的库,否则它只是没有用(请注意,这是针对空问题的 _all_ 解决方案,而不仅仅是您的)。

最后,您可能会成功地扭曲类型系统以避免内置null类型,并将我们想要的检查附加到您的Nothing类型。

在这一点上,你不觉得语言应该支持它吗? 该语言可以以几乎相同的方式(在内部)实现所有内容。 但最重要的是,您将拥有良好的语法,消除边缘情况,不需要外部短绒,并且肯定会被 3rd 方定义更好地采用。 这不是更有意义吗?

在这一点上,你不觉得语言应该支持它吗?

让我们谈谈我的想法。 我认为明天在 TypeScript 具有不可为空类型的世界中醒来会很好。 事实上,这是我每天晚上睡觉的想法。 但它永远不会在第二天发生,我感到沮丧。 然后我去工作并一遍又一遍地遇到同样的空值问题,直到最近我决定寻找一种可以让我的生活更轻松的模式。 看来我找到了。 我是否仍然希望我们在 TypeScript 中有不可空值? 我当然会做。 没有他们和沮丧,我能活下去吗? 好像可以

我是否仍然希望我们在 TypeScript 中有不可空值? 我当然会做。

那你为什么要关闭这个issue呢? :笑脸:

话虽如此,应该关闭请求,因为不再存在问题。 案件结案,下课。

我很高兴您找到了满足您需求的解决方案,但和您一样,我仍然希望有一天 TS 得到一些适当的支持。 而且我相信这仍然可能发生(不要很快,请注意)。 C# 团队目前正在做同样的头脑风暴,也许这可能有助于推进事情。 如果 C# 7 设法获得非空类型(目前还不确定),那么 TS 没有理由不这样做。

@aleksey-bykov

所以为了简化

var nothing: void = null;
function isNothing<a>(value: a | void): value is void {
    return value == null;
}
var something = Math.random() > 0.5 ? 'hey!' : nothing;

现在您也可以在没有任何函数包装器的情况下使用它 - 正确的类型定义就足够了。

这基本上是最初让我有点高兴的解决方法,直到不鼓励依赖它

您必须查看所有定义文件并放入 ! 在适当情况下。 那有什么不同?

使用标准的官方语言功能而不是 hacks 的主要区别在于,社区 (DefinitelyTyped) 可以帮助编写、测试和共享定义文件,而不是将所有额外的工作堆积在您的项目之上(实际上是:微笑:)。

我提倡“ --noImplicitNull ”。 这将使所有类型默认为不可为空,立即提供有关任何现有代码中潜在问题的反馈。 如果有“!”,它应该像 Swift 一样运行,让类型检查器忽略空值的可能性(不安全转换为不可空值)——尽管这与ES7+ 的“最终发送”运算符相冲突,因此它可能不是最好的主意。

如果这在向后兼容性破坏方面看起来过于激进,那真的是我的错。 我真的应该找时间在叉子里试一试,以证明它并不像看起来那么激进。 事实上添加“!” 更激进:它需要更多的更改才能利用它,因为大多数值并不是真正的空值; --noImplicitNull更加渐进,因为您将通过修复错误地声明非空值的类型定义来发现更多错误,如果没有这些修复,您将获得相同的行为,除了空分配、未初始化的值和忘记检查可选对象字段.

在讨论中加入我的声音,我喜欢 Kotlin 如何解决这个问题,即

  • 默认情况下,自己的变量没有可以为空的类型,一个 '?' 改变了
  • 外部变量可以为空,一个 '!!' 覆盖
  • 空检查是一种类型转换
  • 外部代码可以通过生成注释的工具进行分析

比较:
1
2

  • 如果a|void被禁止可以切换到a|Nothing

使用标准的官方语言功能而不是 hacks 的主要区别在于社区 (DefinitelyTyped) 可以帮助编写、测试和共享定义文件

  • 不明白为什么使用 100% 合法的功能是一种黑客行为
  • 不要高估社区的技能,许多(如果不是大多数)定义质量都很低,甚至像 jquery 这样的定义通常不能在没有一些改动的情况下使用
  • 再说一次,模式永远不会像成熟的功能一样好,毫无疑问,希望每个人都能理解
  • 一个模式可以解决你今天的问题,而一个特征可能会或可能不会接近
  • 如果通过使用 AB 和 C 可以有效地(高达 95%)解决某种情况,为什么还有人需要 D 来做同样的事情? 想吃糖? 为什么我们不专注于模式无法解决的问题?

无论如何,有问题解决者和完美主义者,正如所显示的问题是可以解决的

(如果你不想依赖 void 类型,Nothing 也可以是class Nothing { private toString: any; /* other Object.prototype members */ }也有同样的效果。见#1108)

_注意:这里没有什么是关于自行车棚的语法。 请不要拿
像这样。_

我会简单地说:这是提出的 TS 1.6 解决方案的问题
此处:它可以可行地工作,但不适用于标准库或
大多数定义文件。 它也不适用于数学运算符。 如果你想
抱怨冗余类型/运行时样板,尝试做泛型
集合在 C++ 中工作 - 相比之下,这相形见绌,特别是如果你想要
惰性迭代或不是数组的集合(或者更糟糕的是,尝试
两者结合)。

@aleksey-bykov 目前提出的解决方案的问题是
它没有在语言核心中强制执行。 例如,你不能做
Object.defineProperty(null, "name", desc) - 你会得到一个 TypeError
null目标对象。 另一件事:您不能将任何属性分配给
null对象,就像在var o = null; o.foo = 'foo'; 。 你会得到一个
参考错误 IIRC。 这个提议的解决方案无法解释这些情况。
这就是我们需要语言支持的原因。


_现在,有点温和的自行车棚..._

至于语法,我喜欢“?string”的简洁性和
"!string" 语法,但就最终结果而言,我不在乎,只要我
不写ThisIsANullableType<T, U, SomeRidiculousAndUnnecessaryExtraGeneric<V, W>>

在星期三,2015年7月29日,16:10阿列克谢贝科夫[email protected]写道:

  • 如果 a|void 被禁止可以切换到 a|Nothing

    使用标准官方语言功能时的主要区别
    而不是黑客是社区(DefinitelyTyped)在那里提供帮助
    编写、测试和共享定义文件

    不明白为什么使用 100% 合法的功能是一种黑客行为

    不要高估社区的技能,很多(如果不是大多数)
    那里的定义质量很低,甚至像 jquery 这样的定义

    通常不能在没有一些改动的情况下按书面形式使用

    再说一次,模式永远不会像成熟的功能一样好,希望

    毫无疑问,每个人都明白

    一个模式可以解决你今天的问题,而一个功能可能或可能

    不要靠近

    如果使用 AB 可以有效地(高达 95%)解决某种情况
    和 C 为什么有人需要 D 做同样的事情? 想吃糖? 为什么会
    我们专注于模式无法解决的事情?

无论如何,有问题解决者和完美主义者,正如所显示的
问题是可以解决的


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -126082053
.

在我们纠结要涂什么颜色之前,我们能否至少了解一下自行车棚结构会是什么样子? 到目前为止,在前 10 条评论之后,我看到的大部分内容是“嘿,我们想建一个自行车棚!我们应该把它涂成什么颜色?” 没有什么要确保设计在结构上是合理的。 (请参阅 #3192 和 #1206 的前半部分,了解这方面的其他几个示例 - 当我最终提出一个具有逻辑创建的语法和完全指定的语义的严肃提案时,大部分噪音都消失了。)

请记住:这很可能会导致对标准库类型定义进行重大重构和部分重写。 这也将导致需要为支持此功能的第一个版本的 TS 重写绝对类型上的大多数类型定义。 所以请记住,这肯定会被打破。 (一种解决方案可能是默认情况下始终发出,即使发生与空值相关的错误,但提供一个标志 a la --noImplicitAny以更改该行为。)

不可否认,我在这里有点不知所措。 然而,我在scholar.google 中搜索了“nullable javascript”和“nullability javascript”:
理解打字稿
在 TypeScript(模块泛型)中有一个很好的类型表。

JavaScript 的依赖类型
似乎很重要。 源代码消失了

信任,但要验证:动态的两阶段打字语言
脚本语言的细化类型
提供基于打字稿的解决方案。
(“简写 t?代表 t + null。”;听起来像 #186 )

@afrische其中大部分已经在 J​​S 类型检查器中实际使用。 例如,Flow 使用了“信任,但验证”中的大部分习语。 还有Infernu ,这是一个 WIP JS 类型检查器,主要依赖 Hindley-Milner 类型推断1来推断其类型。 但我离题了...

[1] Haskell、OCaml 等也为他们的类型系统使用了修改版本。

我目前正在玩一个 TS 叉,它假定类型默认情况下不可为空,这使得(现有的)空类型可以用null引用,例如string | null 。 我还为string?添加了语法,这只是相同联合类型的快捷方式。

当您深入思考它时,它与@aleksey-bykov 相同,但null是内置的Nothing类型。

这甚至并不复杂,因为类型系统已经非常擅长处理所有这些!

事情变得棘手的地方是我们想要一个平滑的过渡路径。 我们需要一些向后兼容性,_至少_与现有的定义文件(与项目本身的兼容性可能是一个选择加入的标志,尽管如果一段代码的含义——比如在互联网上——没有的话会更好” t 取决于全局编译器标志)。

@afrische的使用理念string?在自己的项目,但使用string!.d.ts可能是一个新的方法。 虽然我不太喜欢它创造的任意二元性。 为什么string空而某些文件在其他文件中不可为空? 看起来很奇怪。

如果不想在某些文件中而不是在其他文件中字符串可以为空。
我喜欢@impinball的想法,即在编译器中有一个选择退出标志并将其引入为重大更改(喘息:与我之前的论点相比发生了重大变化)。 当然,假设新语法在使用 opt-out 标志时不会导致错误。

这个帖子已经有一年多了,很难弄清楚所有相关的设计和关注点是什么,有近 300 条评论,只有少数来自 TS 团队本身。

我计划将大型代码库迁移到 Flow、Closure Compiler 或 TypeScript,但 TypeScript 中缺乏 null 安全性是一个真正的交易破坏者。

在没有阅读整个线程的情况下,我无法弄清楚添加!?空性说明符同时保持缺少它们的类型的现有行为有什么问题,所以这里是一个建议:

提议

declare var foo:string // nullability unspecified
declare var foo:?string // nullable
declare var foo:!string // non-nullable

?string!string ?string的超集, string两者的超集和子集,即string?string!stringany和所有其他类型的关系相同,即没有!?的裸类型就像any关于可空性。

以下规则适用:

| 类型 | 包含 | 提供 | 可以分配null吗? | 可以假设不是null吗? |
| --- | --- | --- | --- | --- |
| T | T , !T , ?T | T , ?T , !T | 是 | 是 |
| ?T | T , !T , ?T | T , ?T | 是 | 否(需要类型保护)|
| !T | T , !T | T , ?T , !T | 没有| 是 |

这在不破坏现有代码的情况下提供了空安全,并使现有代码库能够增量引入空安全,就像any使现有代码库能够增量引入类型安全一样。

例子

这是一些带有空引用错误的代码:

function test(foo:Foo) { foo.method(); }
test(null);

此代码仍然通过。 null仍然可以分配给Foo并且Foo仍然可以假定为非空。

function test(foo:!Foo) { foo.method(); }
test(null);

现在test(null)是错误的,因为null不能分配给!Foo

function test(foo:?Foo) { foo.method(); }
test(null);

现在foo.bar()是错误的,因为?Foo上的方法调用是不允许的(即代码必须检查null )。

谢谢! 我真的很喜欢这个主意。 虽然,为了兼容性,
我们可以让?TT只是别名,比如Array<T>T[]吗? 它
会简化事情,未修饰的版本在技术上仍然是
可以为空。

在星期五,2015年8月28日,07:14杰西沙尔肯[email protected]写道:

这个线程已经一年多了,很难弄清楚所有的
相关设计和关注点有近 300 条评论,只有少数
来自 TS 团队本身。

我计划将大型代码库迁移到 Flow、Closure Compiler 或
TypeScript 但 TypeScript 缺乏类型安全是一个真正的破坏者。

没有阅读整个线程,我无法弄清楚出了什么问题
添加两者! 和 ? 可空性说明符,同时保持
缺少它们的类型的现有行为,所以这里是一个建议:
提议

声明 var foo:string // 可空性未指定declare var

?string 是 !string 的超集,string 是超集和子集
两者的关系,即字符串与 ?string 和 !string 的关系
与 any 和所有其他类型之间的关系相同,即一个
裸型没有! 或者 ? 就可空性而言,就像 any 一样。

以下规则适用:
类型包含提供可以赋值为空吗? 可以假设不为空吗? TT, !T, ?TT,
?T, !T 是 是 ?TT, !T, ?TT, ?T 是 否 (需要类型保护) !TT, !TT,
?T, !T 不 是

这在不破坏现有代码的情况下提供了空安全,并启用
现有代码库逐步引入空安全,就像任何
使现有的代码库能够逐步引入类型安全。
例子

这是一些带有空引用错误的代码:

功能测试(富:富){ foo.method(); }测试(空);

此代码仍然通过。 null 仍然可以分配给 Foo 并且 Foo 仍然可以
被假定为非空。

功能测试(富:!富){ foo.method(); }测试(空);

现在 test(null) 出错了,因为 null 不能分配给 !Foo。

功能测试(富:?富){ foo.method(); }测试(空);

现在 foo.bar() 出错了,因为不允许对 ?Foo 进行方法调用
(即代码必须检查空值)。


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -135743011
.

@impinball

谢谢! 我真的很喜欢这个主意。 虽然,为了兼容性,我们可以让?TT只是别名,比如Array<T>T[]吗? 它会简化事情,并且未修饰的版本在技术上仍然可以为空。

_整个想法_是T?T!T是三个不同的东西,并且_is_ 为了兼容性(与现有 TS 代码的兼容性)。 我不知道怎么解释更好,抱歉。 我的意思是,我做了一张小桌子和所有东西。

好的。 好点子。 我误解了那部分的表格,我忽略了
现有定义文件切换的情况,导致出现问题
其他应用程序和库。

在星期五,2015年8月28日,11:43杰西沙尔肯[email protected]写道:

@impinball https://github.com/impinball

谢谢! 我真的很喜欢这个主意。 虽然,为了兼容性,
我们可以让 ?T 和 T 只是别名,比如 Array和T[]? 它会
简化事情,未修饰的版本在技术上仍然可以为空。

_整个想法_是T,?T和!T是三个不同的东西,
并且 _is_ 出于兼容性考虑(与现有 TS 的兼容性
代码)。 我不知道怎么解释更好,抱歉。 我的意思是,我做了一个
小桌子和一切。


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -135810311
.

抱歉我是个聪明人,但这没有意义

?string!string ?string的超集,而string两者的超集和子集

从集合论中我们知道集合 A 是集合 B 的一个子集,并且只有当 A = B 时,集合 B 的超集才是

如果是这样,当你说“两者”时,它只能意味着everything-of-?string = everything-of-string 和everything-of-!string = everything-of-string

所以最终everything-of-?string = everything-of-!string

每个人都下车的地方,公交车哪儿也去不了,公交车停运了

@aleksey-bykov 可能是措辞不当的情况。 我认为他的意思是string更像是?string | !string ,采用最宽松的约束。

这整个事情在范围上类似于 Java 越来越常见的@Nullable@NonNull / @NotNull编译时类型注释,以及它们如何处理未注释的类型。

而且我肯定会赞成为类型被隐式视为不可为空的新标志,特别是原语。

“默认情况下不可为空”标志会很好,但它也会破坏大量的绝对类型定义。 这包括 Angular 和 Node 的定义,需要大量繁琐的工作来修复。 它还可能需要向后移植,因此新类型仍然不会解析为语法错误,但不会检查可空性。 这种向后移植将是减轻此类标志损坏的唯一实用方法,尤其是在为可空类型更新定义时。 (人们仍然在开发中使用 TypeScript 1.4,尤其是在较大的项目中。)

@impinball

人们仍然在开发中使用 TypeScript 1.4,尤其是在较大的项目中。

我认为这个功能的兼容性故事已经足够难了,而无需尝试使新定义与旧编译器兼容(FWIW 非空类型添加到当前的 TS 编译器几乎是微不足道的)。

如果人们继续使用旧的 TS,那么他们应该使用旧的定义。 (我知道这不切实际)。
我的意思是无论如何事情都会破裂。 很快 TS 1.6 将添加使用它的交集类型和定义也不兼容。

顺便说一句,我很惊讶人们仍然使用 1.4,1.5 中有很多值得爱的地方。

@aleksey-bykov

抱歉我是个聪明人,但这没有意义

我假设您称自己为“聪明人”,因为您从文章的其余部分非常清楚“两者的超集和子集”的含义。 当然,如果应用于实际集合是无意义的。

any可以分配任何类型(它表现为所有类型的超集)并被视为任何类型(它表现为所有类型的子集)。 any表示“选择退出对该值的类型检查”。

string可以从!string?string赋值(它表现为它们的超集)并且可以被视为!string?string (它表现为它们的一个子集)。 string表示“选择退出对该值的可空性检查”,即当前的 TS 行为。

@impinball

而且我肯定会赞成为类型被隐式视为不可为空的新标志,特别是原语。

@RyanCavanaugh明确表示:

改变语言语义的标志是一件危险的事情。 [...] 重要的是,查看一段代码的人可以“跟随”类型系统并理解正在做出的推断。 如果我们开始有一堆改变语言规则的标志,这将变得不可能。

唯一安全的做法是保持可赋值性的语义相同,并更改错误与不依赖于标志的错误,就像今天noImplicitAny工作方式一样。

这就是为什么我的提议保留了缺少可为空性说明符的类型( !? )的现有行为的原因。 要添加的 _only_ 安全标志将禁止缺少可空性说明符的类型,即它只需要传递代码并导致它出错,它不会改变其含义。 我认为noImplicitAny是出于同样的原因被允许的。

@杰西沙尔肯

我的意思是在未初始化的隐式类型变量的上下文中
在这些情况下到nullundefined

var a = new Type(); // type: !Type
var b = 2; // type: !number
var c = 'string'; // type: !string
// etc...

对困惑感到抱歉。

在星期五,2015年8月28日,19:54杰西沙尔肯[email protected]写道:

@impinball https://github.com/impinball

我肯定会赞成为类型设置一个新标志
隐式地视为不可为空,尤其是原语。

@RyanCavanaugh https://github.com/RyanCavanaugh明确表示:

改变语言语义的标志是一件危险的事情。 [...]
看一段代码的人可以“跟上”很重要
使用类型系统并了解正在进行的推断。 如果
我们开始有一堆改变语言规则的标志,
这变得不可能。

唯一安全的做法是保持语义
可分配性相同并更改错误与不依赖的内容
在旗帜上,很像 noImplicitAny 今天的工作方式。

这就是为什么我的建议保持现有行为的类型缺乏
可空性说明符(!或?)。 要添加的 _only_ 安全标志是
一种不允许缺少可空性说明符的类型,即它只
接受传递的代码并导致它出错,它不会改变它的含义。 一世
假设 noImplicitAny 出于同样的原因被允许。


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -135916676
.

@jeffmcaffer你试图通过改变单词 superset 和 subset 的原始含义来表达的内容仍然可以很容易地用集合来表达: string的值集是!string值的并集!string?string表示来自!string或/和?string任何东西都属于string

@impinball同样,这样的标志会改变现有代码的含义,这是不允许的(从阅读@RyanCavanaugh的评论)。 export var b = 5;现在将导出一个!number ,之前它导出一个number

是的,某种意义上。 为了更技术一点,它接受联合,但是
它提供了交叉点。 基本上,任何一种类型都可以算作
stringstring可以为任一类型传递。

在星期五,2015年8月28日,20:20阿列克谢贝科夫[email protected]写道:

@impinball https://github.com/impinball你想说什么
改变单词 superset 和 subset 的本义仍然可以
很容易用集合来表达:字符串的值集合是一个
!string 和 ?string 值的 _union_ 表示来自 !string 的任何内容
或/和 ?string 属于 string


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -135920233
.

显然,它并不意味着默认开启。 和大多数定义文件
将不受影响。 从技术上讲,任何不纯粹的改变
附加(即使向函数添加新参数也不在 JavaScript 中)具有
破坏应用程序的能力。 它的范围类似于
noImplicitAny因为它强制更明确的输入。 和我
不要相信它会打破更多,尤其是因为唯一的
这可能影响其他文件的方式是通过从实际的 TypeScript 导出
源文件。 (另一个标志破坏了许多定义文件,并禁用了
一种常见的鸭子测试方式。)

在星期五,2015年8月28日,20:21杰西沙尔肯[email protected]写道:

@impinball https://github.com/impinball同样,这样的标志会改变
现有代码的含义,其中(来自阅读@RyanCavanaugh
https://github.com/RyanCavanaugh 的评论)是不允许的。 出口变量
b = 5; 现在将导出一个 !number 之前它导出了一个数字。


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -135920315
.

显然,它并不意味着默认开启。

它仍在改变代码的含义。 一旦你这样做了,你就不能阅读一些 TypeScript 代码并说“我知道这意味着什么”,也不能编写一个别人会使用的库,因为代码的含义取决于使用什么标志编译它,这是不可接受的。 你真的把语言一分为二。

这就是为什么我的提议为缺少可空性说明符( ?! )的类型保留现有行为的原因。

@杰西沙尔肯
如果您希望(并设法)以 100% 保真度保留现有代码的含义,那么您应该放弃打字稿空安全的想法。 没有任何解决方案在实践中可用。

必须将?!放在每个类型注释上已经足够冗长了,我不想这样做,但是您还必须放弃所有类型推断。 let x = 3推断今天的number 。 如果您不接受此处的更改,则意味着您必须明确键入所有内容才能获得空安全的好处。 也不是我愿意做的事情。

当利大于弊时,可以做出一些让步。 正如@impinball所指出的,TS 团队对noImplicitAny正是这样做的。 这是一个标志,当您打开它时,它会在您的代码中产生新的错误。 因此,如果您接收的代码不是在noImplicitAny假设下编写的(这发生在我身上),那么从 Internet 复制粘贴代码,甚至仅使用 TS 库都可能会中断。

我认为可以以类似的方式引入空安全。 代码的含义是相同的,并且以完全相同的语义运行。 但是在一个新的标志下(比如noImplicitNull或其他),你会从不是用noImplicitNull假设编写的代码中得到额外的检查和警告/错误。

我认为可以以类似的方式引入空安全。 代码的含义是一样的
它以完全相同的语义运行。 但是在一个新的标志下(比如 noImplicitNull 或其他什么)
你会从不是用的代码中得到额外的检查和警告/错误
noImplicitNull 假设。

我喜欢这种方法,这似乎是一种发展语言的合乎逻辑的方式。 我希望随着时间的推移,它会成为事实上的标准,就像通常在编写打字时考虑 noImplicitAny 一样。

但是,我认为就逐步采用而言,重要的是确保现有代码可以一次迁移一个模块,并且考虑到显式空值编写的新代码可以轻松地与使用隐式空值的现有代码一起使用。

那么这个怎么样:

  • 使用 -noImplicitNull, T成为!T的别名。 否则T可空性是未知的。
  • 通过在顶部添加注释,该标志应该可以在每个模块的基础上被覆盖,例如。 <strong i="18">@ts</strong>:implicit_null 。 这类似于 Flow 在每个模块的基础上启用类型检查的方式。
  • 转换现有代码库是通过首先添加 -noImplicitNull 编译器选项,然后使用“ @ts :implicit_null”标志注释所有现有模块来完成的。 第二步可以简单地自动化。
  • 导入模块时,如果导入的模块和导入的模块具有不同的隐式设置,则需要有关于如何转换类型的策略。

如果具有显式空值的模块导入具有隐式空值的模块,则最后一点有不同的选项。

  • 一种极端的方法是将T类型的可空性视为未知,并要求导入器将类型显式转换为?T!T 。 这将需要在被调用者中进行大量注释,但要小心。
  • 另一种方法是将所有导入的T类型视为?T 。 这也需要调用者中的大量注释。
  • 最后,所有导入的T类型都可以视为!T 。 这在某些情况下当然是错误的,但它可能是最务实的选择。 类似于将any类型的变量分配给T类型的值的方式。

想法?

@jods4 noImplicitAny标志_不会_改变现有代码的含义,它只要求代码明确表示本来是隐含的内容。

| 代码 | 旗帜 | 含义 |
| --- | --- | --- |
| interface Foo { blah; } | | interface Foo { blah:any; } |
| interface Foo { blah; } | noImplicitAny | 错误,需要显式类型 |
| var foo = 'blah' | | var foo:string = 'blah' |
| var foo = 'blah' | noImplicitNull | var foo:!string = 'blah' |

使用noImplicitNull ,在您拥有可以写入 null 的变量之前。 现在你有一个null _cannot_ 被写入的变量。 这与noImplicitAny完全不同。

@RyanCavanaugh已经排除了改变现有代码语义的标志。 如果您打算完全忽略 TS 团队的明确要求,那么这张票将再存在一年。

@jesseschalken抱歉,我看不出区别。
noImplicitAny之前,您的代码中可能包含以下内容:
let double = x => x*2;
它编译并运行良好。 但是一旦你打开noImplicitAny ,编译器就会向你抛出一个错误,说x是隐含的。 您必须修改代码以使其与新标志一起使用:
let double = (x: any) => x*2或更好的let double = (x: number) => x*2
请注意,尽管编译器引发了错误,但它仍会发出完美运行的 JS 代码(除非您关闭出现错误时的发出)。

在我看来,空值的情况几乎相同。 让我们假设使用新标志进行讨论, T是不可为空的,并且T?T | null表示Tnull
之前你可能有:
let foo: string; foo = null;或什至只是let foo = "X"; foo = null这将被推断为string只是一样的。
它编译并运行良好。 现在打开新的noImplicitNull标志。 突然 TS 抛出一个错误,表明您不能将null分配给未明确声明的内容。 但除了打字错误之外,您的代码仍会发出_相同的_、正确的 JS 代码。
使用标志,您需要明确说明您的意图并修改代码:
string? foo; foo = null;

那么有什么区别,真的吗? 代码总是很好地发出,它的运行时行为根本没有改变。 在这两种情况下,您都会从类型系统中得到错误,并且您必须修改您的代码以在您的类型声明中更加明确以摆脱它们。

此外,在这两种情况下,都可以采用严格标志下编写的代码并在关闭标志的情况下编译它,它仍然可以正常工作并且没有错误。

@robertknight非常接近我目前的想法。

对于没有选择严格非空类型的模块/定义, T基本上应该意味着:关闭此类型上的所有类型的空错误。 试图将其强制为T?仍然会产生兼容性问题。

问题是今天有些T实际上是不可为空的,而有些是可以为空的。 考虑:

// In a strict module, function len does not accept nulls
function len(x: string): number { return x.length; }
// In a legacy module, some calls to len
let abc: string = "abc";
len(abc);

如果在遗留模块中将string别名string? ,则调用将成为错误,因为您将可能为空的变量传递给不可为空的参数。

@jods4再次阅读我的评论。 看看桌子。 我不知道如何更清楚地表达它。

通过将foo的定义和foo的赋值放在彼此旁边,您的示例是明确制作的以得出您的结论。 使用noImplicitAny ,导致的唯一错误是特定于需要更改的代码(因为它没有改变其含义,它只需要更明确地表达它)。 对于noImplicitNull ,导致错误的代码是对foo的 _assignment_ 但需要更改以修复它的代码(具有旧含义)是foo的 _definition_ foo _ 定义的含义。 赋值和定义显然可以位于库边界的不同侧,为此noImplicitNull标志改变了该库公共接口的_含义_!

该标志改变了 foo 定义的含义。

是的,这是真的。 它从“我完全不知道变量是否可以为空——我只是不在乎”到“这个变量是空的”。 正确的可能性为 50/50,如果不正确,您必须在声明中明确您的意图。 最后,结果与noImplicitAny :您必须在声明中使您的意图更加明确。

分配和定义显然可以位于库边界的不同侧

实际上,通常是库中的声明和我的代码中的使用。 现在:

  1. 如果库选择了严格空值,那么它必须正确声明其类型。 如果库说它有严格的空类型并且x是不可为空的,那么我试图分配一个空值_确实是一个应该报告的错误_。
  2. 如果库没有选择严格空值,那么编译器不应该为其使用引发任何错误。

如果不调整代码,则无法打开此标志(就像noImplicitAny )。

我明白你的意思,但我想说我们不会_改变_含义代码; 相反,我们_表达了今天的类型系统没有处理的含义。

这不仅很好,因为它会在今天的代码中捕获错误,而且我想说如果不采取这样的步骤,TS 中将永远不会有可用的非空类型。

不可空类型的好消息! 看起来 TS 团队可以在 TS 更新中引入重大更改!
如果你看到这个:
http://blogs.msdn.com/b/typescript/archive/2015/09/02/annoucing-typescript-1-6-beta-react-jsx-better-error-checking-and-more.aspx
它们引入了具有新文件类型的重大更改(互斥的可选语法),并且引入了 _没有_新文件类型的重大更改(影响所有人)。
这是我们可以争论不可空类型的先例(例如,.sts 或严格的 Typescript 扩展和相应的 .sdts)。

现在我们只需要弄清楚我们是希望编译器尝试检查未定义的类型还是空类型(以及什么语法),并且我们有一个可靠的建议。

@jbondc非常有趣的阅读。 很高兴看到我关于迁移到 NNBD(默认情况下不可为空)比迁移到可选的不可为空更容易的直觉已被研究证实(迁移的变化减少了一个数量级,在 Dart 的情况下,1-每 1000 行代码需要 2 个注释进行空性更改,即使在空重代码中也不超过 10 个)

我不确定文档的复杂性是否真的反映了不可为空类型的复杂性。 例如,在泛型部分,他们讨论了 3 种形式类型参数,然后表明您实际上并不需要它们。 在 TS 中, null只是完全空记录的类型(没有属性,没有方法),而{}将是根非空类型,然后不可为空的泛型只是G<T extends {}> - 根本不需要讨论多种形式类型参数。

此外,他们似乎提出了很多非必需糖,例如var !x

不过,对已经处理过相同问题的现有语言的调查是真正的瑰宝。

阅读文档我意识到Optional / Maybe类型比可空类型更强大,尤其是在泛型上下文中 - 主要是因为能够对Just(Nothing)进行编码。 例如,如果我们有一个通用的 Map 接口,它包含T类型的值并支持get ,它可能会或可能不会根据键的存在返回值:

interface Map<T> {
  get(s:string):Maybe<T>
}

没有什么可以阻止T成为Maybe<U> ; 如果键存在但包含Nothing值,代码将运行良好并返回Just(Nothing)如果键根本不存在,将简单地返回Nothing

相反,如果我们使用可空类型

interface Map<T> {
  get(s:string):T?
}

那么当T为空时,就不可能区分丢失的键和空值。

无论哪种方式,区分可空值和不可空值并相应地为可用方法和属性建模的能力是任何类型安全的先决条件。

@jbondc这是一个非常

令人欣慰的是,研究表明 80% 的声明实际上意味着非空,或者每个 KLOC 只有 20 个无效注释(第 21 页)。 正如文档中所指出的,这是默认情况下非 null 的有力论据,这也是我的感觉。

有利于非空的另一种说法是,它创造一个更清洁型系统: null是它自己的类型, T为非空和T?是一个代名词T | null 。 因为 TS 已经有联合类型,一切都很好,干净并且运行良好。

看到最近解决这个问题的语言列表,我真的认为一种新的现代编程语言应该处理这个长期存在的问题并减少代码库中的空错误。 对于应该在类型系统中建模的东西来说,这是一个太常见的问题。 我仍然希望 TS 有一天会得到它。

我发现运算符T!的想法很有趣,而且可能很有用。 我在想一个系统,其中T是非空类型, T?T | null 。 但令我困扰的是,即使面对空输入,您也无法真正创建一个保证非空结果的通用 API。 我没有很好的用例,但理论上我不能忠实地建模: function defined(x) { return x || false; }
使用非空反转运算符,可以这样做: function defined<T>(x: T): T! | boolean 。 这意味着如果defined返回T它保证是非空的,即使通用的T约束是可以为空的,比如string? 。 而且我认为在 TS 中建模并不难:给定T! ,如果T是包含null类型的联合类型,则返回删除null产生的类型来自工会的

@spion

阅读文档我意识到 Optional / Maybe 类型比可空类型更强大

你可以嵌套Maybe结构,你不能嵌套null ,确实。

在定义新 api 的上下文中,这是一个有趣的讨论,但是在映射现有 api 时,几乎没有选择。 使语言映射 Nulls 到Maybe不会利用这个好处,除非函数被完全重写。

Maybe两条不同的信息进行编码:是否有值以及值是什么。 以您的Map示例并查看 C#,这很明显,来自Dictionary<T,K>
bool TryGet(K key, out T value)
请注意,如果 C# 有元组(可能是 C# 7),这基本上与以下内容相同:
(bool hasKey, T value) TryGet(K key)
这基本上是一个Maybe并允许存储null

请注意,JS 有自己的处理这个问题的方法,它创造了很多新的有趣问题: undefined 。 如果未找到键,典型的 JS Map将返回undefined ,否则返回其值,包括null

你们确实意识到问题没有解决,除非你以同样的方式对 undefined 建模?
否则,您所有的空问题都将被未定义的问题替换(无论如何,imo 更为普遍)。

@格里福克

你所有的空问题都会被未定义的问题所取代

不,他们为什么要?
我的空问题将消失,我未定义的问题将继续存在。

确实, undefined仍然是一个问题。 但这在很大程度上取决于您的编码风格。 我几乎没有使用undefined编码,除非浏览器将它们强加给我,这意味着我的 90% 的代码使用空检查会更安全。

我几乎没有未定义的代码,除非浏览器将它们强加给我

我原以为 JavaScript 会在每一轮都强制undefined为一个。

  • 未初始化的变量。 let x; alert(x);
  • 省略函数参数。 let foo = (a?) => alert(a); foo();
  • 访问不存在的数组元素。 let x = []; alert(x[0]);
  • 访问不存在的对象属性。 let x = {}; alert(x['foo']);

另一方面,空值发生在更少且更可预测的情况下:

  • DOM 访问。 alert(document.getElementById('nonExistent'));
  • 第三方网络服务响应(因为JSON.stringify去掉了undefined )。 { name: "Joe", address: null }
  • 正则表达式。

出于这个原因,我们禁止使用null ,将所有通过网络收到的null转换为undefined ,并且始终对undefined使用严格的相等性检查。 这在实践中对我们很有效。

因此,我确实同意undefined问题是更普遍的问题。

@NoelAbrahams编码风格,我告诉你 :)

未初始化的变量

我总是初始化变量并且我打开了noImplicitAny ,所以无论如何let x都会是一个错误。 我在我的项目中使用的越接近let x: any = null ,尽管这是我不会经常编写的代码。

可选功能参数

我对可选参数使用默认参数值,在我看来这更有意义(您的代码 _will_ 以某种方式读取并使用参数,不是吗?)。 所以对我来说: function f(name?: string = null, options?: any = {})
访问原始undefined参数值对我来说是一个 _exceptional_ 情况。

访问不存在的数组元素

这是我努力不在我的代码中做的事情。 我检查我的数组长度是否越界,并且我不使用稀疏数组(或尝试使用默认值填充空槽,例如null0 ,...)。
同样,您可能会想出一个特殊情况,我会这样做,但这将是一个_例外_,而不是规则。

访问不存在的对象属性。

与数组几乎相同。 我的对象被输入,如果一个值不可用,我将它设置为null ,而不是undefined 。 同样,您可能会发现边缘情况(例如进行字典探测),但它们是_边缘情况_,并不代表我的编码风格。

在我得到undefined返还的所有 _exceptional_ 情况下,我会立即对undefined结果采取行动,并且不会进一步传播它或使用它“工作”。 具有空检查的虚构 TS 编译器中的典型真实示例:

let cats: Cat[];
// Note that find returns undefined if there's no cat named Kitty
let myFavoriteCat = cats.find(c => c.name === 'Kitty'); 
if (myFavoriteCat === undefined) {
  // Immediately do something to compensate here:
  // return false; or 
  // myFavoriteCat = new Cat('Kitty'); or
  // whatever makes sense.
}
// Continue with assurance that myFavoriteCat is not null (it was an array of non-nullable cats after all).

出于这个原因,我们禁止使用 null ,将所有通过网络接收到的 null 转换为 undefined ,并且始终对 undefined 使用严格的相等性检查。 这在实践中对我们很有效。

由此我了解到您使用的编码风格与我非常不同。 如果您基本上到处都使用undefined那么是的,您将从静态检查的空类型中受益比我少得多
将。

是的,由于undefined原因,事情不是 100% 不漏水的,我不相信有人可以创建一种在这方面 100% 正确的合理可用的语言。 JS 在太多地方引入了undefined

但我希望你能从我上面的回答中看出,使用适当的编码风格,_很多_可以从空检查中受益。 至少我的观点是,在我的代码库中,它将有助于发现和防止许多愚蠢的错误,并在我的团队环境中提高生产力。

@jods4 ,阅读您的方法很有趣。

我认为我对这种方法的反对意见是,似乎有很多规则需要遵守——这更像是共产主义与自由市场:smile:

TS团队内部有一个和我们类似的规则,他们自己的风格。

@NoelAbrahams “使用undefined ”和“使用null ”一样是一条规则。

在任何情况下,一致性是关键,我不喜欢一个项目,我不确定事情应该是null还是undefined (或者一个空字符串或零)。 特别是因为 TS 目前对这个问题没有帮助......

我知道 TS 有一条规则支持undefined不是null ,我很好奇这是一个任意的“为了一致性”的选择,或者这个选择背后是否有更多的论据。

为什么喜欢使用null而不是undefined

  1. 它以我们的开发人员熟悉的方式工作,其中许多来自静态 OO 语言,例如 C#。
  2. 未初始化的变量在许多语言中通常被视为代码异味,不知道为什么它在 JS 中应该有所不同。 明确你的意图。
  3. 虽然 JS 是一种动态语言,但使用不改变的静态类型性能更好。 将属性设置为null比将其设置delete更有效。
  4. 它支持的清洁差异null其定义,但表示没有价值, undefined ,这是...不确定的。 两者区别很明显的一个地方:可选参数。 不传递参数会导致undefined 。 如果您在代码库中使用undefined来_pass_空值? 使用null这里没有问题。
  5. 它为空检查开辟了一条干净的路径,如本线程中所述,这对于undefined来说并不实用。 虽然也许我在做白日梦。
  6. 您必须为一致性做出选择,恕我直言nullundefined一样好。

我认为更喜欢undefinednull的原因是
默认参数和与obj.nonexistentProp返回的一致性
undefined

除此之外,我不明白什么算作空
足以要求变量可以为空。

2015 年 9 月 8 日,星期二,06:48 jods [email protected]写道:

@NoelAbrahams https://github.com/NoelAbrahams “使用未定义”为
就像“使用空值”这样的规则。

无论如何,一致性是关键,我不喜欢我所在的项目
不确定事物是否应该为空或未定义(或空的
字符串或零)。 特别是因为 TS 目前对此没有帮助
问题...

我知道 TS 有一条规则支持 undefined 而不是 null,我很好奇这是否
是一个任意的“为了一致性”的选择,或者如果有更多
选择背后的争论。

为什么 _I_ 喜欢使用 null 而不是 undefined:

  1. 它以我们的开发人员熟悉的方式工作,许多来自静态 OO
    C#等语言。
  2. 未初始化的变量通常被视为代码异味
    许多语言,不知道为什么它应该在 JS 中有所不同。 做你的
    意图明确。
  3. 虽然 JS 是一种动态语言,但性能更好
    不变的静态类型。 将属性设置为更有效
    null 比删除它。
  4. 它支持定义的 null 之间的干净差异,但
    表示没有值,并且未定义,即……未定义。
    两者区别明显的地方:可选
    参数。 不传递参数会导致未定义。 你怎么
    _pass_ 空值,如果您在代码中使用 undefined
    根据? 使用 null 这里没有问题。
  5. 它为空检查开辟了一条干净的道路,如本文所述
    线程,这对于 undefined 来说并不实用。 虽然也许
    我在做白日梦。
  6. 您必须为一致性做出选择,恕我直言 null 与
    不明确的。


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -138514437
.

@impinball
我们可以在每个 github 讨论中停止使用“bikeshedding”吗? 我们可以肯定地说,使用undefinednull是团队偏好; 但是是否尝试在空检查中包含undefined以及它如何工作的问题并非微不足道。 所以我不明白这首先是如何进行自行车棚的?

我已经构建了一个具有不可为空类型的 TS 1.5 分支,它非常简单。 但我认为有两个难题需要达成共识才能在官方 TS 编译器中拥有不可为空的类型,上面已经详细讨论了这两个问题,但没有明确的结论:

  1. 我们如何处理undefined ? (我的意见:它仍然无处不在且未经检查)
  2. 我们如何处理与现有代码的兼容性,特别是定义? (我的意见:选择加入标志,至少每个定义文件。打开标志是“破坏”,因为您可能必须添加空注释。)

我个人的意见是应该处理 null 和 undefined
同样出于可空性的目的。 两者都用于该用例,
表示没有值。 一是价值从未存在,
另一个是价值曾经存在,不再存在。 两个都
应该算作可空性。 大多数 JS 函数返回 undefined,但很多
DOM 和库函数返回 null。 两者都服务于相同的用例。 因此,
他们应该得到同等对待。

自行车棚参考是关于代码样式参数的
应该用来表示没有值。 有些人在争论
只是 null,有些人认为只是未定义,有些人认为
混合物。 该提案不应仅限于其中之一。

2015 年 9 月 8 日,星期二,13:28 jods [email protected]写道:

@impinball https://github.com/impinball
我们可以在每个 github 讨论中停止使用“bikeshedding”吗? 我们可以安全地
说使用 undefined 或 null 是团队偏好; 但问题
是否尝试在空检查中包含未定义(或不​​包含)以及如何
它会起作用不是微不足道的。 所以我不明白这在自行车棚里是怎样的
第一名?

我已经构建了一个不可为空类型的 TS 1.5 分支,它是
出奇的容易。 但我认为有两个棘手的问题
需要在官方 TS 编译器中达成不可为空类型的共识,
两者都在上面进行了详细讨论,但没有明确的结论:

  1. 我们用 undefined 做什么? (我的观点:它仍然无处不在
    和未选中)
  2. 我们如何处理与现有代码的兼容性,特别是
    定义? (我的意见:选择加入标志,至少每个定义文件。
    打开标志是“破坏”,因为您可能必须添加 null
    注释。)


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -138641395
.

@jods4我从你的第 2 点中了解到,默认情况下,你的 fork 也推断出不可为空的类型,而不是需要显式的不可为空类型注释?

请问可以链接吗? 我想试试看。

@impinball
我也很想看到(一些)针对undefined安全性,但它非常普遍。

特别是,我们可以定义一个不可为空类型的数组吗?
鉴于越界(或稀疏)数组访问返回undefined ,我们是否可以方便地定义和使用数组?
我认为要求所有数组都可以为空在实践中是一种负担。

我们应该区分nullundefined类型吗? 这并不难: T | nullT | undefinedT | null | undefined并且可以为上述问题提供一个简单的答案。 但是那么速记语法呢: T?代表什么? null 和 undefined ? 我们需要两种不同的速记吗?

@Arnavion
Null 和 Undefined 类型已经存在于 TS 中。
我的做法是:

  1. 使所有类型不可为空(包括推断类型);
  2. 为空类型指定一个名称( null ),您可以在类型声明中使用它;
  3. 删除从nullany的加宽;
  4. 引入与T | null相同的语法简写T? T | null
  5. 删除从null到任何其他类型的隐式转换。

无法访问我的资源,我认为这就是它的要点。 现有的 Null 类型和美妙的 TS 类型系统(尤其是联合类型和类型保护)完成剩下的工作。

我还没有在 github 上提交我的工作,所以我现在不能分享链接。 我想先编写兼容性开关,但我一直忙于其他事情:(
兼容性开关涉及更多<_<。 但这很重要,因为现在 TS 编译器自己编译时会出现很多错误,并且很多现有测试都失败了。
但它似乎实际上在全新的代码上运行良好。

让我总结一下到目前为止我从一些评论者那里看到的东西,希望我能说明这个对话是如何循环的:
问题:某些“特殊情况”值发现它们被放入并非旨在处理它们的代码中,因为它们与语言中的所有其他类型(即 null 和未定义)的处理方式不同。
我:你为什么不制定编程标准,这样这些问题就不会发生?
其他:因为如果意图可以反映在打字中,那就太好了,因为这样一来,在打字稿中工作的每个团队都不会对其进行不同的记录,并且会发生关于 3rd 方库的错误假设。
Everone:我们将如何处理这个问题?
其他:让我们让空值更严格并使用标准来处理未定义!

当 undefined 远比 null 问题更严重时,我不明白这怎么可能被认为是一个解决方案!

免责声明:这并不能反映这里每个人的态度,只是说得够多了,我想强调这一点。
唯一可以接受的方法是,如果它是一个问题的解决方案,而不是解决问题的一小部分的一点点解决方案。

当电话!
*每个人

*它已经足够了,我想突出显示它。

最后一段也应改为“此提案的唯一方式”

@jods4我会说这取决于某些结构。 在某些情况下,您可以保证在这些情况下不可为空,例如:

declare const list: T![];

for (const entry of list) {
  // `entry` is clearly immutable here.
}

list.forEach(entry => {
  // `entry` is clearly immutable here.
})

list.map(entry => {
  // `entry` is clearly immutable here.
})

在这种情况下,编译器必须有大量的逻辑来确保数组在边界内检查:

declare const list: T![]

for (let i = 0; i < list.length; i++) {
  // This could potentially fail if the compiler doesn't correctly do the static bounds check.
  const entry: T![] = list[i];
}

在这种情况下,您无法保证编译器可以验证对 get entry是否在边界内,而无需实际评估部分代码:

declare const list: T![]

const end = round(max, list.length);

for (let i = 0; i < end; i++) {
  const entry: T![] = list[i];
}

有一些简单、明显的,但也有一些更难的。

@impinball事实上,现代 API 诸如mapforEachfor..of是可以的,因为它们会跳过从未初始化或删除的元素。 (它们确实包括已设置为undefined元素,但我们假设的空安全 TS 会禁止这样做。)

但是经典的数组访问是一个重要的场景,我希望看到一个好的解决方案。 除了微不足道的情况(但重要的情况,因为它们很常见)之外,显然不可能按照您的建议进行复杂的分析。 但请注意,即使您可以证明i < array.length也不能证明该元素已初始化。

考虑以下示例,您认为 TS 应该做什么?

let array: T![] = [];  // an empty array of non-null, non-undefined T
// blah blah
array[4] = new T();  // Fine for array[4], but it means array[0..3] are undefined, is that OK?
// blah blah
let i = 2;
// Note that we could have an array bounds guard
if (i < array.length) {
  let t = array[i];  // Inferred type would be T!, but this is actually undefined :(
}

Object.defineProperty 也存在问题。

let array = new Array(5)
Object.defineProperty(array, "length", 2, {
  get() { return 10 },
})

在星期三,2015年9月9日,17:49 jods [email protected]写道:

@impinball https://github.com/impinball实际上,现代 API,例如 map,
forEach 或 for..of 都可以,因为它们会跳过从未出现过的元素
初始化或删除。 (它们确实包括已设置为
未定义,但我们假设的 null-safe TS 会禁止这样做。)

但是经典的数组访问是一个重要的场景,我想
看到一个很好的解决方案。 按照您的建议进行复杂的分析是
显然是不可能的,除非在微不足道的情况下(但重要的情况下
它们很常见)。 但是请注意,即使您可以证明 i <
array.length 它不能证明该元素已初始化。

考虑以下示例,您认为 TS 应该做什么?

让数组:T![] = []; // 一个非空的、非未定义的 T 的空数组// blah blah
数组[4] = new T(); // 数组[4] 很好,但它意味着数组[0..3] 未定义,这样可以吗?// blah blahlet i = 2;// 注意我们可以有一个数组边界guardif (i < array.长度) {
让 t = 数组 [i]; // 推断类型将是 T!,但这实际上是未定义的 :(
}


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -139055786
.

您需要做的就是:

  1. 使 Null 和 Undefined 类型可引用。
  2. 防止将 null 和 undefined 值分配给任何东西。
  3. 使用带有 Null 和/或 Undefined 的联合类型,其中我们暗示了可空性。

这确实是一个大胆的突破性变化,看起来更适合一种语言
延期。
2015 年 9 月 10 日上午 9:13,“Isiah Meadows”通知@ github.com 写道:

Object.defineProperty 也存在问题。

let array = new Array(5)
Object.defineProperty(array, "length", 2, {
get() { return 10 },
})

在星期三,2015年9月9日,17:49 jods [email protected]写道:

@impinball https://github.com/impinball确实,现代 API 如
地图,
forEach 或 for..of 都可以,因为它们会跳过从未出现过的元素
初始化或删除。 (它们确实包括已设置为
未定义,但我们假设的 null-safe TS 会禁止这样做。)

但是经典的数组访问是一个重要的场景,我想
看到一个很好的解决方案。 按照您的建议进行复杂的分析是
显然是不可能的,除非在微不足道的情况下(但重要的情况下
它们很常见)。 但是请注意,即使您可以证明 i <
array.length 它不能证明该元素已初始化。

考虑以下示例,您认为 TS 应该做什么?

让数组:T![] = []; // 一个非空的、非未定义的 T 的空数组//
呸呸
数组[4] = new T(); // 对 array[4] 很好,但这意味着 array[0..3] 是
未定义,可以吗?// blah blahlet i = 2;// 注意我们可以有一个
数组边界guardif (i < array.length) {
让 t = 数组 [i]; // 推断类型是 T!,但这实际上是
不明确的 :(
}


直接回复此邮件或在 GitHub 上查看
<
https://github.com/Microsoft/TypeScript/issues/185#issuecomment-139055786>
.


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -139230568
.

@impinball
我对你的例子感觉很好。 以这种方式使用defineProperty是跨出 TS 安全盒,进入动态 JS 领域,你不觉得吗? 我认为我从未在 TS 代码中直接调用过defineProperty

@aleksey-bykov

看起来更适合语言扩展。

该提案的另一个问题是,除非它被广泛接受,否则它的价值会降低。
最终,我们需要更新 TS 定义,如果它是 TS 的很少使用、不兼容、扩展或分支,我认为它不会发生。

我确实知道类型化数组确实有保证,因为 IIRC 他们抛出一个
越界数组加载和存储时的 ReferenceError。 常规数组和
当索引超出范围时,arguments 对象返回 undefined。 在我的
意见,这是一个 JS 语言缺陷,但修复它肯定会
打破网络。

“修复”这个问题的唯一方法是在 ES6 中,通过构造函数返回一个代理,
并将其实例原型和自身原型设置为原始数组
构造函数。 像这样的东西:

Array = (function (A) {
  "use strict";
  function check(target, prop) {
    const i = +prop;
    if (prop != i) return target[prop];
    if (i >= target.length) {
      throw new ReferenceError();
    }
    return i;
  }

  function Array(...args) {
    return new Proxy(new Array(...args), {
      get(target, prop) {
        return target[check(target, prop)];
      },
      set(target, prop, value) {
        return target[check(target, prop)] = value;
      },
    });
  }

  Array.prototype = A.prototype;
  Array.prototype.constructor = Array
  Object.setPrototypeOf(Array, A);
  return Array;
})(Array);

(注意:它未经测试,是在手机上输入的......)

在星期四,2015年9月10日,10:09 jods [email protected]写道:

@impinball https://github.com/impinball
我对你的例子感觉很好。 以这种方式使用defineProperty是
走出 TS 安全箱,进入动态 JS 领域,不要
您认为? 我认为我从未在 TS 代码中直接调用过defineProperty。

@aleksey-bykov https://github.com/aleksey-bykov

看起来更适合语言扩展。

该提案的另一个问题是,除非它被广泛接受
降低了价值。
最终,我们需要更新 TS 定义,我认为不会
如果它是 TS 的很少使用、不兼容、扩展或分支,就会发生这种情况。


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -139245706
.

@impinball不确定首先要“修复”某些东西...
这些是 JS 的语义,TS 必须以某种方式适应它们。

如果我们只是有一个(略有不同的)标记来声明稀疏数组和非稀疏数组,并使非稀疏数组自动初始化(也许程序员可以提供一个值或等式进行初始化),该怎么办? 这样我们就可以强制稀疏数组为T|undefined类型(使用 for... of 和其他“安全”操作将更改为 T 类型)并单独保留非稀疏数组的类型。

//not-sparse
var array = [arrlength] => index*3;
var array = <number[]>[3];
//sparse
var array = [];

显然这不是最终的语法。
第二个示例将每个值初始化为该类型的编译器默认值。
这确实意味着对于非稀疏数组,您必须键入它们,否则我怀疑您必须在它们完全初始化后将它们转换为某些东西。
此外,我们应该需要一个非稀疏类型的数组,以便程序员可以将他们的数组转换为非稀疏类型。

@格里福克
我不知道...它变得混乱。

这样我们就可以强制稀疏数组类型T|undefined (使用 for... of 和其他“安全”操作将更改为类型 T)

由于 JS 中的一个怪癖,它不能那样工作。 假设let arr: (T|undefined)[]
所以我可以自由地做: arr[0] = undefined
如果我这样做,那么使用那些“安全”函数 _will_ 为第一个插槽返回undefined 。 所以在arr.forEach(x => ...)你不能说x: T 。 它仍然必须是x: T|undefined

第二个示例将每个值初始化为该类型的编译器默认值。

这在精神上不是很像 TS。 也许我错了,但在我看来,TS 哲学是类型只是 JS 之上的一个附加层,它们不会影响代码生成。 为了我不太喜欢的部分类型正确性,这具有性能影响。

TS 显然不能保护您免受 JS 中的一切影响,并且您可以从有效的 TS 调用几个函数/构造,它们对对象的运行时类型具有动态影响并破坏静态 TS 类型分析。

如果这是类型系统中的一个漏洞会不会很糟糕? 我的意思是,这段代码真的不常见: let x: number[] = []; x[3] = 0;如果这是你想做的事情,那么也许你应该声明你的数组let x: number?[]

它并不完美,但我认为它对于大多数现实世界的使用来说已经足够了。 如果你是一个纯粹主义者,想要一个健全的类型系统,那么你当然应该看看另一种语言,因为 TS 类型系统无论如何都不是健全的。 你怎么认为?

这就是为什么我说您还需要能够转换为非稀疏数组
类型,所以你可以自己初始化一个数组而没有性能
影响。
我会满足于(什么是)稀疏之间的区别
数组和非稀疏数组按类型。

对于那些不知道为什么这很重要的人,这与您的原因相同
想要TT|null之间的区别。

在上午09点11分,周五,2015年9月11日jods [email protected]写道:

@Griffork https://github.com/Griffork
我不知道...它变得混乱。

这样我们就可以强制稀疏数组的类型为 T|undefined(这将
使用 for... of 和其他“安全”操作更改为 T 类型)

由于 JS 中的一个怪癖,它不能那样工作。 假设让 arr:
(T|未定义)[]。
所以我可以自由地做:arr[0] = undefined。
如果我这样做,那么使用那些“安全”函数_will_ return undefined
对于第一个插槽。 所以在 arr.forEach(x => ...) 你不能说 x: T。
它仍然必须是 x: T|undefined。

第二个示例将每个值初始化为编译器默认值
对于那种类型。

这在精神上不是很像 TS。 也许我错了,但在我看来
TS 的哲学是类型只是 JS 之上的一个附加层
并且它们不会影响代码生成。 这对性能有影响
我不太喜欢的部分类型正确性。

TS 显然不能保护您免受 JS 中的一切影响,并且有
您可以从有效 TS 调用的几个函数/构造,它们具有
对对象运行时类型的动态影响并打破静态 TS
类型分析。

如果这是类型系统中的一个漏洞会不会很糟糕? 我的意思是,这段代码
真的不常见:让 x: number[] = []; x[3] = 0; 如果那是
你想做的事情那么也许你应该声明你的数组让
x:数字?[]。

它并不完美,但我认为它对于大多数现实世界的使用来说已经足够了。
如果你是一个想要声音类型系统的纯粹主义者,那么你当然应该
看看另一种语言,因为 TS 类型系统无论如何都不健全。 什么
你认为?


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -139408240
.

@jods4我所说的“修复”是“修复”什么是 JS 语言设计缺陷,恕我直言。 不是 TypeScript,而是 _JavaScript 本身_。

@jods我在抱怨 JS,而不是 TS。 我承认这是题外话。

在星期四,2015年9月10日,19:19 Griffork [email protected]写道:

这就是为什么我说您还需要能够转换为非稀疏数组
类型,所以你可以自己初始化一个数组而没有性能
影响。
我会满足于(什么是)稀疏之间的区别
数组和非稀疏数组按类型。

对于那些不知道为什么这很重要的人,这与您的原因相同
想要TT|null之间的区别。

在上午09点11分,周五,2015年9月11日jods [email protected]写道:

@Griffork https://github.com/Griffork
我不知道...它变得混乱。

这样我们就可以强制稀疏数组的类型为 T|undefined(这将
使用 for... of 和其他“安全”操作更改为 T 类型)

由于 JS 中的一个怪癖,它不能那样工作。 假设让 arr:
(T|未定义)[]。
所以我可以自由地做:arr[0] = undefined。
如果我这样做,那么使用那些“安全”函数_will_ return undefined
对于第一个插槽。 所以在 arr.forEach(x => ...) 你不能说 x: T。
它仍然必须是 x: T|undefined。

第二个示例将每个值初始化为编译器默认值
对于那种类型。

这在精神上不是很像 TS。 也许我错了,但在我看来
TS 的哲学是类型只是一个附加层
JS
并且它们不会影响代码生成。 这对性能有影响
我不太喜欢的部分类型正确性。

TS 显然不能保护您免受 JS 中的一切影响,并且有
您可以从有效的 TS 调用的几个函数/构造,它们

对对象运行时类型的动态影响并打破静态 TS
类型分析。

如果这是类型系统中的一个漏洞会不会很糟糕? 我的意思是,这段代码
真的不常见:让 x: number[] = []; x[3] = 0; 如果那是
你想做的事情那么也许你应该声明你的数组

x:数字?[]。

它并不完美,但我认为它对于大多数现实世界的使用来说已经足够了。
如果你是一个想要声音类型系统的纯粹主义者,那么你应该
当然
看看另一种语言,因为 TS 类型系统无论如何都不健全。
什么
你认为?


直接回复此邮件或在 GitHub 上查看
<
https://github.com/Microsoft/TypeScript/issues/185#issuecomment-139408240>
.


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -139409349
.

至于我关于数组长度的声明,我们可以假设所有数组访问都在边界内,并且除非在接口中明确指定,否则越界访问是未定义的。 这与 C/C++ 所做的非常相似,如果有人决定最终编写一个使用语言规范的第三方编译器,但它并不关心匹配发射。

我知道支持匹配的 C/C++ 未定义行为在表面上听起来很愚蠢,但我认为在这种情况下,它可能是值得的。 很少看到通过越界访问实际完成_更好_的事情。 我见过的 99.99% 的使用都是非常刺鼻的代码气味,几乎总是由几乎不熟悉 JavaScript 的人完成的。

(根据我的经验,这些人中的大多数人甚至没有听说过 CoffeeScript,更不用说 TypeScript。他们中的许多人甚至不知道刚刚完成并标准化的新版 JS,即 ES2015。)

对此是否有新的解决方案?

缺少不可为空的类型,如果尝试访问 _assuredly_ null 变量上的属性,TypeScript 失败似乎仍然很有用。

var o = null;
console.log(o.x);

......应该失败。

通过类型系统确保所有数组访问都经过边界检查,这似乎进入了依赖类型的领域。 虽然依赖类型非常简洁,但这似乎是一个比不可为空类型更大的特性。

假设在编译时不对数组强制执行边界检查,似乎有三个选项:

  1. 数组索引(以及任何通过索引访问的任意数组元素)被视为返回可空类型,即使在不可为空类型的数组上也是如此。 本质上, [] “方法”具有T?的类型签名。 如果您知道自己只是在进行边界检查索引,则可以在应用程序代码中将T?转换为T!
  2. 数组索引返回与数组的泛型类型参数完全相同的类型(具有相同的可空性),并且假定所有数组访问都由应用程序进行边界检查。 越界访问将返回undefined ,并且不会被类型检查器捕获。
  3. 核心选项:所有数组都被硬编码为可空的语言,并且尝试使用不可为空的数组会失败类型检查。

这些都适用于基于索引的对象属性访问,例如object['property']其中object的类型{ [ key: string ]: T! }

我个人更喜欢第一个选项,其中对数组或对象进行索引返回可空类型。 但即使是第二个选项似乎也比一切都可以为空要好,这是当前的状态。 核选项很糟糕,但老实说也比一切都可以为空要好。

第二个问题是类型是否应该默认为不可为空,或默认为可空。 似乎在任何一种情况下,对于显式可空类型和显式不可空类型都具有语法以处理泛型是有用的; 例如,想象一个容器类(例如 Map)上的get方法,它接受一些值并可能返回一个类型,_即使容器只包含不可为空的值_:

class Container<K,V> {
  get(key: K): V? {
    // fetch from some internal data structure and return the value, if it exists
    // return null otherwise
  }
}

// only non-nullable values allowed in the container
const container = new Container<SomeKeyClass!, SomeValueClass!>();
const val: SomeValueClass!;
// ... later, we attempt to read from the container with a get() call
// even though only non-nullables are allowed in the container, the following should fail:
// get() explicitly returns null when the item can't be found
val = container.get(someKey);

类似地,我们可能(这不是一个强有力的论点)希望确保我们的容器类只接受插入时的非空键,即使在使用可空键类型时也是如此:

class Container<K, V> {
  insert(key: K!, val: V): void {
    // put the val in the data structure
    // the key must not be null here, even if K is elsewhere a nullable type
  }
}

const container = new Container<SomeKeyClass?, SomeValueClass>();
container.insert(null, new SomeValueClass()); // fails

因此,无论默认值是否更改,对于可空类型和不可为空类型都具有显式语法似乎很有用。 除非我错过了什么?

在两者都有语法的地方,默认值似乎可能是类似于--noImplicitAny的编译器标志。 就个人而言,我会投票支持在 2.0 版本发布之前保持不变的默认设置,但只要有逃生舱口(至少是暂时的),两者似乎都不错。

我更喜欢第二个选项,因为即使它会导致越界访问未定义的行为(在 TS 类型领域),我认为这是一个很好的妥协。 它可以大大提高速度,而且处理起来更简单。 如果您真的希望越界访问是可能的,您应该显式使用可空类型,或将结果强制转换为可空类型(这总是可能的)。 通常,如果数组不可为空,任何越界访问几乎总是一个错误,一个应该在某个时候猛烈爆发的错误(这是一个 JS 缺陷)。

它几乎要求程序员明确他们的期望。 它的类型安全性较差,但在这种情况下,我认为类型安全性最终可能会成为障碍。

以下是与每个选项的比较,以求和函数为例(原语是最有问题的):

// Option 1
function sum(numbers: !number[]) {
  let res = 0
  for (let i = 0; i < numbers.length; i++) {
    res += <!number> numbers[i]
  }
  return res
}

// Option 2
function sum(numbers: !number[]) {
  let res = 0
  for (let i = 0; i < numbers.length; i++) {
    res += numbers[i]
  }
  return res
}

// Option 3
function sum(numbers: number[]) {
  let res = 0
  for (let i = 0; i < numbers.length; i++) {
    res += <!number> numbers[i]
  }
  return res
}

另一个例子: map函数。

// Option 1
function map<T>(list: !T[], f: (value: !T, index: !number) => !T): !T[] {
  let res: !T[] = []
  for (let i = 0; i < list.length; i++) {
    res.push(f(<!T> list[i], i));
  }
  return res
}

// Option 2
function map<T>(list: !T[], f: (value: !T, index: !number) => !T): !T[] {
  let res: !T[] = []
  for (let i = 0; i < list.length; i++) {
    res.push(f(list[i], i));
  }
  return res
}

// Option 3
function map<T>(list: T[], f: (value: !T, index: !number) => !T): T[] {
  let res: T[] = []
  for (let i = 0; i < list.length; i++) {
    const entry = list[i]
    if (entry !== undefined) {
      res.push(f(<!T> entry, i));
    }
  }
  return res
}

另一个问题:每一个中entry的类型是什么? !string?stringstring

declare const regularStrings: string[];
declare const nullableStrings: ?string[];
declare const nonnullableStrings: !string[];

for (const entry of regularStrings) { /* ... */  }
for (const entry of nullableStrings) { /* ... */  }
for (const entry of nonnullableStrings) { /* ... */  }

选项三有点诙谐的建议:stuck_out_tongue:

回复:你的最后一个问题:

declare const regularStrings: string[];
declare const nullableStrings: string?[];
declare const nonNullableStrings: string![]; // fails typecheck in option three

for(const entry of regularStrings) {
  // option 1: entry is of type string?
  // option 2: depends on default nullability
}

for(const entry of nullableStrings) {
  // option 1 and 2: entry is of type string?
}

for(const entry of nonNullableStrings) {
  // option 1: entry is of type string?
  // option 2: entry is of type string!
}

在某些情况下——例如,你想要返回一个不可为空的类型并且你是从一个数组中获取它——假设你在其他地方保证没有未定义的值,你必须使用选项一进行额外的转换在数组中(无论采用哪种方法,此保证的要求都不会改变,只需要键入as string! )。 就我个人而言,我仍然更喜欢它,因为它更明确(您必须指定何时采取可能危险的行为,而不是隐式发生)并且与大多数容器类的工作方式更加一致:例如,地图的get函数明确返回可空类型(如果它存在于键下,则返回对象,否则返回 null),如果Map.prototype.get返回可空类型,则object['property']应该可以相同,因为它们对可空性做出了类似的保证,并且用法类似。 这使得数组成为奇怪的数组,其中空引用错误可能会重新出现,并且类型系统允许随机访问不可为空。

肯定还有其他方法; 例如,当前 Flow 使用选项二,最后我检查了SoundScript 在其规范中明确将稀疏数组设为非法(好吧,强模式/“SaneScript”使它们非法,并且 SoundScript 是新规则的超集),这在某种程度上尽管他们仍然需要弄清楚如何处理手动长度更改和初始分配,但回避了这个问题。 我怀疑他们会在便利性与安全性的权衡中更接近选项一——也就是说,编写起来不太方便但更安全——因为他们强调类型系统的健全性,但它可能看起来有些不同由于禁止使用稀疏数组,因此比这两种方法都好。

我认为性能方面在这一点上是非常理论化的,因为 AFAIK TypeScript 将继续发出相同的 JS,不管这里的任何选择如何转换,并且底层 VM 将继续在引擎盖下检查数组。 所以我并没有被这个论点所左右。 我脑子里的问题主要是关于便利性还是安全性; 对我来说,这里的安全胜利似乎值得权衡便利。 当然,要么是对所有类型都可以为空的改进。

我同意性能部分主要是理论性的,但我仍然会
喜欢假设的方便。 大多数数组是密集的,并且可空性为
default 对于布尔数组和数字数组没有意义。 如果不是
打算成为一个密集数组,它应该被标记为显式可为空的
意图很明确。


TypeScript 确实需要一种断言的方法,因为断言通常是
用于辅助其他语言的静态类型检查。 我曾在
V8 代码基于UNREACHABLE();宏,允许假设为
更安全一点,如果不变量被违反,程序就会崩溃。 C++
static_assert用于静态断言以帮助进行类型检查。

2015 年 10 月 20 日,星期二,凌晨 4:01,Matt Baker通知@github.com
写道:

选项三有点诙谐的建议 [图片:
:stuck_out_tongue:]

回复:你的最后一个问题:

声明 const regularStrings: string[];declare const nullableStrings: string?[];declare const nonNullableStrings: string![]; // 选项三中的类型检查失败
for(regularStrings 的常量条目){
// 选项 1:条目是字符串类型?
// 选项 2:取决于默认的可空性
}
for(nullableStrings 的常量条目){
// 选项 1 和 2:条目的类型是字符串?
}
for(nonNullableStrings 的常量条目){
// 选项 1:条目是字符串类型?
// 选项 2:条目是字符串类型!
}

在某些情况下——你想返回一个不可为空的类型,而你
例如,从数组中获取它——你必须进行额外的转换
选项一假设您在其他地方保证没有未定义
数组中的值(此保证的要求不会改变
无论采用哪种方法,只需要输入字符串即可!)。
就我个人而言,我仍然更喜欢它,因为它更明确(你必须
指定您何时采取可能危险的行为,而不是它
隐式发生)并且与大多数容器类的方式更加一致
工作:例如,Map 的 get 函数明确返回可空类型
(如果该对象存在于键下,则返回该对象,否则返回 null),
如果 Map.prototype.get 返回可空值,则 object['property']
可能应该做同样的事情,因为他们做出了类似的保证
可空性和用法类似。 这将数组作为奇数排除在外
空引用错误可以重新出现的地方,以及随机访问的地方
允许类型系统不可为空。

肯定还有其他方法; 例如,当前 Flow 使用
选项二http://flowtype.org/docs/nullable-types.html ,最后我
检查 SoundScript 使稀疏数组在其规范中明确非法
https://github.com/rwaldron/tc39-notes/blob/master/es6/2015-01/JSExperimentalDirections.pdf
(好吧,强模式/“SaneScript”使它们非法,而 SoundScript 是
新规则的超集),这在某种程度上回避了问题
尽管他们仍然需要弄清楚如何处理手动长度
变化和初始分配。

我认为性能方面在这一点上非常理论化,
因为 AFAIK TypeScript 将继续发出相同的 JS,而不管
在这里进行任何选择,底层虚拟机将继续
无论如何,引擎盖下的边界检查数组。 所以我不会太动摇
那个论点。 我脑子里的问题主要是关于便利性还是便利性。
安全; 对我来说,这里的安全胜利似乎值得权衡便利。 的
当然,要么是对所有类型都可以为空的改进。


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -149468527
.

以赛亚草甸

我们应该开始称它们non-void类型吗? 无论如何,我认为用T!!T明确定义non-void !T是错误的。 作为人类很难阅读,也很难处理 TypeScript 编译器的所有情况。

我在默认情况下看到的non-void类型的主要问题是它是一个重大变化。 好吧,如果我们只是添加一些更多的静态分析,类似于 Flow,那根本不会改变行为,但会捕获更多错误呢? 然后我们现在可以捕获这个类的大部分错误,但不要更改语法,并且在将来,引入编译器标志或默认行为会更容易,而不是破坏性更改。

``` .ts
// 函数编译愉快
函数 len(x: string): number {
返回 x.length;
}

len("作品"); // 5
len(空); // 错误,没有空属性长度

``` .ts
function len(x: string): number {
    if (x === null) {
        return -1;
    }
    return x.length;
}

len("works"); // 5
len(null); // null

这里真正要做的是将输入数据建模为non-void ,但在函数中处理时隐式添加void 。 同样,返回类型是non-void除非它可以显式返回nullundefined

我们还可以添加?TT?类型,这会在使用前强制进行空(和/或未定义)检查。 我个人喜欢T? ,但有先例将?T与 Flow 一起使用。

``` .ts
函数 len(x: ?string): number {
返回 x.length; // 错误:类型 ?string 上没有长度属性,您必须使用类型保护
}

One more example -- what about using function results?

``` .js
function len(x: string): number {
    return x.length;
}

function identity(f: string): string {
    return f;
}

function unknown(): string {
    if (Math.random() > 0.5) {
        return null;
    }
    return "maybe";
}

len("works"); // 5
len(null); // error, no property length of null

identity("works"); // "works": string
identity(null); // null: void
unknown(); // ?string

len(identity("works")); // 5
len(identity(null)); // error, no property length of null
len(unknown()); // error: no length property on type ?string, you must use a type guard

在幕后,真正发生的事情是 TypeScript 正在通过查看类型是否处理 null 来推断类型是否可以为 null,并被赋予一个可能的 null 值。

这里唯一棘手的部分是如何与定义文件交互。 我认为这可以通过默认情况下假设声明function(t: T)的定义文件执行空/ void检查来解决,就像第二个例子一样。 这意味着这些函数将能够在编译器不产生错误的情况下采用空值。

这现在允许两件事:

  1. 逐渐采用?T类型语法,其中的可选参数已经被交换。
  2. 将来可以添加编译器标志--noImplicitVoid ,它将声明文件与编译代码文件相同。 这将是“破坏”,但如果在很远的地方完成,当类型可以是voidT时,大多数图书馆将采用使用?T的最佳实践当它不能。 它也可以选择加入,因此只有那些选择使用它的人才会受到影响。 这也可能需要在对象可能为空的情况下使用?T语法。

我认为这是一种现实的方法,因为当源在 TypeScript 中可用时,它可以大大提高安全性,找到那些棘手的问题,同时仍然允许与定义文件的简单、相当直观和向后兼容的集成。

前缀?变体也用于闭包编译器注释 IIRC。

在星期二,2015年11月17日,13:37汤姆·雅克[email protected]写道:

我们应该开始称它们为非空类型吗? 无论如何,我认为
使用 T! 显式定义非空或 !T 是一个错误。 它很难
作为一个人阅读,也很难处理所有的情况
打字稿编译器。

我在默认情况下看到的非空类型的主要问题是它是一个
打破变化。 那么如果我们只是添加一些更多的静态分析,
类似于 Flow,它根本不会改变行为,但会捕获
更多错误? 那么我们现在可以捕获这个类的大部分错误,但是
不改变语法,将来会更容易
引入较少破坏的编译器标志或默认行为
改变。

// 函数编译愉快function len(x: string): number {
返回 x.length;
}

len("作品"); // 5
len(空); // 错误,没有空属性长度

函数 len(x: string): number {
如果(x === null){
返回-1;
}
返回 x.length;
}

len("作品"); // 5
len(空); // 空值

这里真正要做的是将输入数据建模为非空,
但在函数中处理时隐式添加 void 。 相似地,
返回类型是非空的,除非它可以显式返回 null 或
不明确的

我们还可以添加 ?T 或 T? 类型,它强制为空(和/或
未定义)使用前检查。 我个人喜欢 T?,但有先例
将 ?T 与 Flow 一起使用。

函数 len(x: ?string): number {
返回 x.length; // 错误:类型 ?string 上没有长度属性,您必须使用类型保护
}

再举一个例子——如何使用函数结果?

函数 len(x: string): number {
返回 x.length;
}
函数标识(f:字符串):字符串{
返回 f;
}
函数未知():字符串{
如果(Math.random()> 0.5){
返回空;
}
返回“也许”;
}

len("作品"); // 5
len(空); // 错误,没有空属性长度

身份(“作品”); // "作品": 字符串
身份(空); // 空:空
未知(); // ?细绳

len(身份(“作品”)); // 5
len(身份(空)); // 错误,没有空属性长度
len(未知()); // 错误:类型 ?string 上没有长度属性,您必须使用类型保护

在幕后,真正发生的事情是 TypeScript 是
通过查看类型是否处理来推断类型是否可以为空
null,并被赋予一个可能为 null 的值。

这里唯一棘手的部分是如何与定义文件交互。 一世
认为这可以通过默认为假设一个
声明函数的定义文件(t:T)进行空/空检查,很多
就像第二个例子。 这意味着这些功能将能够
null 值,而编译器不会产生错误。

这现在允许两件事:

  1. 逐渐采用 ?T 类型语法,其中可选
    参数已经被交换到。
  2. 将来可以添加编译器标志 --noImplicitVoid
    这会将声明文件与已编译的代码文件相同。 这
    会“打破”,但如果在很远的地方完成,大多数
    当类型可以时,库将采用使用 ?T 的最佳实践
    当它不能时为无效和 T。 它也可以选择加入,所以只有那些
    谁选择使用它会受到影响。

我认为这是一种现实的方法,因为它大大提高了安全性
当源代码在 TypeScript 中可用时,找到那些棘手的问题,
同时仍然允许简单、相当直观和向后兼容
与定义文件的集成。


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -157463734
.

好点子。 与现有的可选参数声明也有很多相似之处:

``` .ts
接口 withOptionalProperty {
o?: 字符串
}
接口 withVoidableProperty {
o: ? 字符串
}

函数 withOptionalParam(o?: string) { }
函数 withVoidableParam(o: ?string) { }
``

实际上,他们确实使用了 prefix 。 他们将?用于显式可空类型, !用于不可空类型,默认

可空与可空的区别很有意义。 :+1:

我觉得这是在兜圈子。

你们都读过上面为什么可撤销/不可撤销的定义不能解决根本问题吗?

@Griffork我已经阅读了每nullundefined是每种类型的一部分,并且编译器目前在尝试时无法确保安全使用T类型的参数。 按照我的例子:

``` .ts
函数 len(x: string): number {
返回 x.length;
}

len("作品");
// 没有错误 -- 5

len(空);
// 编译器允许这样做,但应该在这里出现类似的错误
// 错误:没有 null 的属性 'length'

len(未定义);
// 编译器允许这样做,但应该在这里出现类似的错误
// 错误:没有未定义的属性“长度”
``

这就是我对 _language_ 的根本问题的看法——缺乏安全性。 通过使用静态和类型分析查看传入函数的参数流,并在可能不安全的情况下给出错误,可以修复大量此类问题。 当代码可用时,根本不需要任何?T类型,因为可以执行该分析(尽管并非在所有情况下都始终完全准确)。 添加?T类型的原因是因为它强制进行安全检查,并且使程序员的意图非常明确。

_implementation_ 的问题是向后兼容性。 如果 TS 团队明天发布一个更改,现在默认情况下T类型是非空的,它将破坏当前接受和处理具有这些类型签名的空输入的现有代码。 TS 团队成员在这个问题上已经表示,他们不愿意做出如此大的突破性改变。 他们愿意打破一些东西,如果效果足够小,好处足够大,但这会产生太大的影响。

我的建议分为两个独立的部分,我认为其中一个是对语言的一个很好的补充,除了找到真正的错误之外,它不会改变任何现有的语法/语义,另一个是减少影响的可能方法打破改变以获得未来对非空类型的更强有力的保证。 它仍然是一个突破性的变化,但希望具有更可接受的规模和性质。

第一部分:

  • 添加分析以识别何时可以将 null/undefined/void 类型传递给不处理它们的函数(仅当存在 TS 代码时才有效,而不是在定义文件中)。
  • 添加?T类型,在使用参数之前强制进行无效检查。 这实际上只是围绕语言级选项类型的语法糖。
  • 这两个功能可以独立实现,因为每个功能都有自己的优点

第二部分:

  • 等待。 后来在第一部分介绍后,TS 的社区和用户有时间使用这些功能,标准将是在类型不为空时使用T?T当类型可以为空时。 这不能保证,但我认为这将是一个明显的最佳实践。
  • 作为一个单独的功能,添加一个编译器选项--noImplicitVoid ,它要求类型为?T如果它们可以为空的话。 这只是执行现有最佳实践的编译器。 如果存在不符合最佳实践的定义文件,那将是不正确的,但这就是选择加入的原因。
  • 如果您真的想要严格,该标志可以接受指定它应该应用于哪些目录/文件的参数。 然后您可以仅将更改应用于您的代码,并排除node_modules

我认为这是最现实的选择,因为即使没有第二部分,第一部分也可以完成。 它仍然是一个很好的功能,可以在很大程度上缓解这个问题。 当然它并不完美,但如果这意味着它可能发生,我会接受足够好的。 它还为将来真正的非空类型留下了选项。 现在的一个问题是这个问题持续的时间越长,修复它的破坏性更改就越多,因为在 TS 中编写了更多的代码。 对于第一部分,它在最坏的情况下应该大大减慢速度,最好的情况是随着时间的推移减少影响。

@tejacques我完全

@tejacques - FWIW,我完全同意你的评估和建议。 让我们希望 TS 团队同意:)

其实有两点:

一,我不确定第一部分提到的类似 Flow 的分析是否必要。 虽然它非常酷和有用,但我当然不希望它保留 void/ ?T类型,这在当前的语言设计中似乎更可行,并提供更多的长期价值。

我也会用不同的方式表达--noImplicitVoid - 假设它不允许将nullundefined分配给不可撤销(即默认)类型,而不是“要求类型为?T如果它们可以是无效的”。 我很确定我们的意思是一样的,只是语义; 专注于使用而不是定义,如果我了解 TS 的工作原理,这是它实际上可以强制执行的唯一事情。

刚刚想到的一些事情:我们将有四个级别的可空性(这对 Flow 也是如此,我认为这激发了很多对话):

interface Foo {
  w: string;
  x?: string;
  y: ?string;
  z?: ?string;
}

--noImplicitVoidw只能是有效字符串。 这是类型安全的巨大胜利。 再见,十亿美元的错误! 如果没有--noImplicitVoid ,当然唯一的限制是必须指定它,但它可以为 null 或未定义。 我认为这是该语言的一种相当危险的行为,因为它看起来比实际情况更能保证。

x在当前设置下是完全宽松的。 它可以是字符串、空值、未定义,甚至不需要出现在实现它的对象中。 在--noImplicitVoid ,事情变得有点复杂......你可能没有定义它,但是如果你_do_设置了一些东西,它不能是空的吗? 我认为 Flow 处理这个问题的方式是你可以将xundefined (模仿不存在),但不能设置null 。 不过,这对于 TypeScript 来说可能有点过于自以为是了。

此外,在使用它之前要求对x进行无效检查是合乎逻辑的,但这又将是一个重大变化。 这种行为可能是--noImplicitVoid标志的一部分吗?

y可以设置为任何字符串或空值,但_必须_以某种形式存在,即使是空值。 而且,当然,访问它需要进行无效检查。 这种行为可能有点令人惊讶。 我们是否应该考虑_不_将其设置为与将其设置为undefined ? 如果是这样,除了需要作空检查之外,它与x有何不同?

最后,不需要指定z ,可以设置为绝对任何内容(当然,除了非字符串),并且(有充分的理由)在访问它之前需要进行空检查。 这里一切都有意义!

xy之间有一些重叠,我怀疑x最终会被社区弃用,为了最大的安全性,更喜欢z形式.

@tejacques

当代码可用时根本不需要任何 ?T 类型,因为可以执行该分析(尽管并非在所有情况下都始终完全准确)。

这种说法是不正确的。 即使有完整的代码源可用,也无法解释可空性的许多方面。

例如:当调用一个接口时(静态地),如果接口没有相应的注释,你不知道传递 null 是否正常。 在像 TS 这样的结构类型系统中,甚至很难知道哪些对象是接口的实现,哪些不是。 一般来说,您并不关心,但如果您想从源代码中推断出可空性,您会关心的。

另一个例子:如果我有一个数组list和一个函数unsafe(x)其源代码显示它不接受null参数,编译器不能说线路是否安全: list.filter(unsafe) 。 事实上,除非你能静态地知道list所有可能内容是什么,否则这是无法做到的。

其他情况与继承等有关。

我并不是说标记公然违反空合同的代码分析工具没有价值(确实如此)。 我只是指出,当源代码可用时,淡化空注释的用处是恕我直言的错误。

我在本次讨论的某个地方说过,我认为可空性的推断有助于降低许多简单情况下的向后兼容性。 但它不能完全替代源注释。

@tejacques我不好,我误读了你的评论(出于某种原因,我的大脑决定void===null :( 我责怪刚醒来)。
感谢您的额外帖子,对我来说,它比您原来的帖子更有意义。 我其实很喜欢这个主意。

我将分别回复你们三个,因为在一个帖子上写出来是一个不可读的blob。

@Griffork没问题。 有时很难通过文本正确传达所有内容,我认为无论如何都值得澄清。

@dallonf你的改写正是我的意思——我们在同一页面上。

我认为x?: Ty: ?T之间的区别在于工具提示/函数的使用,以及使用的打字和保护。

声明为可选参数会更改工具提示/函数用法,因此很明显它是可选的:
a?: T在函数调用中不需要作为参数传递,可以留空
如果声明没有?:则必须在函数调用中作为参数传递,并且不能留空

w: T是必需的非void参数(使用--noImplicitVoid
不需要守卫

x?: T是一个可选参数,所以类型真的是T | undefined
需要if (typeof x !== 'undefined')守卫。
注意三重字形!==用于精确检查undefined

y: ?T是必填参数,类型真的是T | void
需要if (y == null)守卫。
注意双字形==匹配nullundefinedvoid

z?: ?T是一个可选参数,类型真的是T | undefined | voidT | void
需要if (z == null)守卫。
再次注意双字形==匹配nullundefinedvoid

与所有可选参数一样,您不能在可选参数之后使用必需参数。 这就是区别所在。 现在,你也可以只对可选参数做一个null保护,这也可以,但一个关键的区别是你不能将null值传递给函数,如果你叫它; 但是,您可以通过undefined

我认为所有这些实际上都有一席之地,因此它不一定会弃用当前的可选参数语法,但我同意z形式是最安全的。

编辑:更新了措辞并修复了一些错别字。

@jods4我几乎同意你所说的一切。 我并不是要淡化非void打字的重要性。 我只是想一次推进一个阶段。 如果 TS 团队以后不能这样做,至少我们会过得更好,如果他们可以在实施更多检查和?T之后继续这样做,那么任务就完成了。

我认为Arrays的情况确实非常棘手。 你总是可以做这样的事情:

``` .ts
函数 numToString(x: number) {
返回 x.toString();
}
var nums: number[] = Array(100);
numToString(nums[0]); // 你完了!

You can try to do something specifically for uninitialized arrays, like typing the `Array` function as `Array<?T>` / `?T[]` and upgrading it to `T[]` after a for-loop initializing it, but I agree that you can't catch everything. That said, that's already a problem anyway, and arrays typically don't even send uninitialized values to `map`/`filter`/`forEach`.

Here's an example -- the output is the same on Node/Chrome/IE/FF/Safari.

``` .ts
function timesTwo(x: number) {
    return x * 2;
}
function all(x) {
    return true;
}
var nums: number[] = Array(100);
nums.map(timesTwo);
// [undefined x 100]
nums.filter(all);
// []
nums.forEach(function(x) { console.log(x); })
// No output

这确实对您没有多大帮助,因为这是出乎意料的,但这在当今真正的 JavaScript 中并不是错误。

我唯一要强调的另一件事是,即使使用接口也可以取得进展,通过静态分析比通过类型系统需要更多的工作和努力,但这与现在已经发生的情况并没有太大不同。

这是一个例子。 假设--noImplicitVoid已关闭

``` .ts
接口 ITransform{
(x: T): U;
}

接口 IHaveName {
名称:字符串;
}

函数变换(x: T, fn: ITransform){
返回 fn(x);
}

var 命名 = {
名称:“福”
};

var 错误名称 = {
名称:1234
};

var namedNull = {
名称:空
};

var someFun = (x: IHaveName) => x.name;
var someFunHandlesVoid = (x: IHaveName) => {
if (x != null && x.name != null) {
返回 x.name;
}
返回“无名”;
};

All of the above code compiles just fine -- no issues. Now let's try using it

``` .ts
someFun(named);
// "Foo"
someFun(wrongName);
// error TS2345: Argument of type '{ name: number; }' is not assignable to parameter
// of type 'IHaveName'.
//   Types of property 'name' are incompatible.
//     Type 'number' is not assignable to type 'string'.
someFun(null);
// Not currently an error, but would be something like this:
// error TS#: Argument of type 'null' is not assignale to parameter of type 'IHaveName'.
someFun(namedNull);
// Not currently an error, but would be something like this:
// error TS#: Argument of type '{ name: null; }' is not assignable to parameter of
// type 'IHaveName'.
//   Types of property 'name' are incompatible.
//     Type 'null' is not assignable to type 'string'.

someFunHandlesVoid(named);
// "Foo"
someFunHandlesVoid(wrongName);
// error TS2345: Argument of type '{ name: number; }' is not assignable to parameter
// of type 'IHaveName'.
someFunHandlesVoid(null);
// "No Name"
someFunHandlesVoid(namedNull);
// "No Name"

transform(named, someFun);
// "Foo"
transform(wrongName, someFun);
// error TS2453: The type argument for type parameter 'T' cannot be inferred from the usage.
// Consider specifying the type arguments explicitly.
//   Type argument candidate '{ name: number; }' is not a valid type argument because it
//   is not a supertype of candidate 'IHaveName'.
//     Types of property 'name' are incompatible.
//       Type 'string' is not assignable to type 'number'.
transform(null, someFun);
// Not currently an error, but would be something like this:
// error TS#: The type argument for type parameter 'T' cannot be inferred from the usage.
// Consider specifying the type arguments explicitly.
//   Type argument candidate 'null' is not a valid type argument because it
//   is not a supertype of candidate 'IHaveName'.
transform(namedNull, someFun);
// Not currently an error, but would be something like this:
// error TS#: The type argument for type parameter 'T' cannot be inferred from the usage.
// Consider specifying the type arguments explicitly.
//   Type argument candidate '{ name: null; }' is not a valid type argument because it
//   is not a supertype of candidate 'IHaveName'.
//     Types of property 'name' are incompatible.
//       Type 'string' is not assignable to type 'null'.

transform(named, someFunHandlesVoid);
// "Foo"
transform(wrongName, someFunHandlesVoid);
// error TS2453: The type argument for type parameter 'T' cannot be inferred from the usage.
// Consider specifying the type arguments explicitly.
//   Type argument candidate '{ name: number; }' is not a valid type argument because it
//   is not a supertype of candidate 'IHaveName'.
transform(null, someFunHandlesVoid);
// "No Name"
transform(namedNull, someFunHandlesVoid);
// "No Name"

你是对的,你不能抓住一切,但你可以抓住很多东西。

最后一点——当--noImplicitVoid开启时,上面的行为应该是什么?

现在someFunsomeFunHandlesVoid都进行了相同的类型检查,并产生与someFun产生的相同的错误消息。 即使someFunHandlesVoid确实处理了 void,用nullundefined调用它也是一个错误,因为签名声明它需要非void 。 它需要输入为(x: ?IHaveName) : string才能接受nullundefined 。 如果我们改变它的类型,那么它会像以前一样继续工作。

这是一个重大更改的部分,但我们需要做的就是在类型签名中添加一个字符?来修复它。 我们甚至可以有另一个标志--warnImplicitVoid ,它的作用与警告相同,因此我们可以慢慢迁移。

我觉得这样做完全是个混蛋,但我会再发一篇文章。

在这一点上,我不知道该怎么做才能继续。 有更好的主意吗? 我们应该吗:

  • 继续讨论/推测这应该如何表现?
  • 把它变成三个新的功能提案?

    • 增强分析

    • 也许/选项类型?T

    • --noImplicitVoid编译器选项

  • ping TypeScript 团队成员以获取输入?

我倾向于新的提议并在那里继续讨论,因为考虑到这个线程需要多长时间,要求 TypeScript 团队赶上这个线程几乎是不人道的。

@tejacques

  • 您在 dallonf 的三重等于示例中缺少typeof
  • 您似乎在 jods4 的示例中缺少一些?

就像我认为这些东西应该留在这个线程中一样,我认为这个线程不再真正被“观看”(也许更像是偶尔瞥一眼)。 因此,创建一些新线程肯定会产生吸引力。
但是要等几天/一周,让人们首先提出意见并提供反馈。 你会希望你的建议非常可靠。

编辑:记住的降价存在。

在这个阶段评论这个线程是非常徒劳的。 即使提出了一个 TypeScript 团队认为可以接受的提案(我在 8 月份尝试过),他们也无法在噪音中找到它。

你所能希望的最好的事情是关注程度足以促使 TypeScript 团队提出他们自己的建议并实施它。 否则,只需忘记它并使用 Flow。

+1 用于拆分,但现在, --noImplicitVoid选项可以
等待可空类型被实现。

到目前为止,我们大部分都在语法和语义上达成了一致
可空类型,所以如果有人可以写出提案和实现
其中,那将是金色的。 我收到了来自类似流程的提案
关于其他类型的枚举,但我只是没有时间
由于其他项目而实施。

2015 年 11 月 18 日,星期三,21:24 Tom Jacques [email protected]写道:

我觉得这样做完全是个混蛋,但我会再做一个
邮政。

在这一点上,我不知道该怎么做才能继续。 有更好的主意吗?
我们应该吗:

  • 继续讨论/推测这应该如何表现?
  • 把它变成三个新的功能提案?

    • 增强分析

    • 可能/选项类型 ?T

    • --noImplicitVoid 编译器选项

  • ping TypeScript 团队成员以获取输入?

我倾向于新的提议并在那里继续讨论
要求 TypeScript 团队赶上这个线程几乎是不人道的
考虑到它有多长。


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/185#issuecomment -157928828
.

+1 为--noImplicitNull选项(禁止 void 和 null 赋值)。

我试图用特殊类型Op<A> = A | NullType来缓解这个问题。 它似乎工作得很好。 见这里

+1 为 _--noImplicitNull_ 以及请:+1:

+1 表示 --noImplicitNull

这应该关闭吗?

@Gaelan Given #7140 已合并,如果您想按照这里的一些人的建议为--noImplicitNull提交一个新的专门问题,那么现在这样做可能是安全的。

@isiahmeadows那时最好

这应该关闭吗?

我们认为https://github.com/Microsoft/TypeScript/issues/2388是这项工作的重命名部分。 这就是我们尚未宣布此功能完成的原因。

如果您想按照这里的一些人的建议为--noImplicitNull提交一个新的专门问题,那么现在这样做可能是安全的。

我不确定我理解这个新标志的请求语义是什么。 我建议用明确的建议开一个新问题。

@mhegazy 本期早些时候为--noImplicitNull提出的想法是,所有内容都必须明确?Type!Type 。 恕我直言,当有另一个标志默认不可为空时,IIRC 已经在可空类型本身被实现时,我认为样板不值得。

现在关闭#7140 和#8010 都合并了。

对不起,如果我对一个已关闭的问题发表评论,但我不知道在哪里提问更好,如果没有兴趣,我认为这不值得一个新问题。
在每个文件的基础上处理隐式 null 是否可行?
就像,用 noImplicitNull 处理一堆 td 文件(因为它们来自确定类型并且是那样构思的),但是将我的源作为隐式空处理?
有人会觉得这有用吗?

@massimiliano-mantione,请参阅https://github.com/Microsoft/TypeScript/issues/8405

此页面是否有帮助?
0 / 5 - 0 等级