Typescript: 建议:在索引签名中包含 undefined 的选项

创建于 2017-01-31  ·  242评论  ·  资料来源: microsoft/TypeScript

更新:对于我的最新提案,请参阅评论https://github.com/Microsoft/TypeScript/issues/13778#issuecomment -406316164

启用strictNullChecks ,TypeScript 不会在索引签名中包含undefined (例如在对象或数组上)。 这是一个众所周知的警告,并在几个问题中进行了讨论,即https://github.com/Microsoft/TypeScript/issues/9235https://github.com/Microsoft/TypeScript/issues/13161https://github .com/Microsoft/TypeScript/issues/ 12287 和https://github.com/Microsoft/TypeScript/pull/7140#issuecomment -192606629。

例子:

const xs: number[] = [1,2,3];
xs[100] // number, even with strictNullChecks

但是,从阅读上述问题来看,许多 TypeScript 用户似乎希望情况并非如此。 诚然,如果索引签名确实包含undefined ,代码可能需要更多的保护,但是对于某些人来说,这是增加类型安全性的可接受的折衷。

包括undefined的索引签名示例:

const xs: number[] = [1,2,3];
xs[100] // number | undefined

我想知道这种行为是否可以被视为strictNullChecks之上的额外编译器选项。 通过这种方式,我们能够满足所有用户组:那些希望在其索引签名中使用或不使用 undefined 进行严格空检查的用户。

Committed Suggestion

最有用的评论

我们仍然非常怀疑任何人是否会在实践中从这个标志中获得任何好处。 地图和类似地图的东西已经可以在其定义站点上选择加入| undefined

TypeScript 的目标之一不是允许在“编译”时捕获错误,而不是依靠用户记住/知道做一些特定的事情吗? 这似乎与那个目标背道而驰; 要求用户做一些事情以避免崩溃。 许多其他功能也是如此。 如果开发人员总是执行 x,则不需要它们。 TypeScript 的目标(大概)是让工作更轻松并消除这些事情。

我遇到了这个错误,因为我在现有代码上启用了strictNullChecks并且我已经进行了比较,所以我得到了错误。 如果我一直在编写全新的代码,我可能不会意识到这里的问题(类型系统告诉我我总是得到一个值)并以运行时失败告终。 依靠 TS 开发人员记住(或者更糟,甚至知道)他们应该用| undefined声明他们的所有地图,感觉就像 TypeScript 没有做人们真正想要它做的事情。

所有242条评论

除了strictNullChecks,我们没有改变类型系统行为的标志。 标志通常启用/禁用错误报告。
您始终可以拥有自定义版本的库,该版本使用| undefined定义所有索引器。 应该按预期工作。

@mhegazy这是一个有趣的想法。 关于如何覆盖数组/对象的类型签名的任何指导?

lib.d.tsinterface Array<T> lib.d.ts 。 我还通过正则表达式\[\w+: (string|number)\]其他索引签名。

有趣,所以我尝试了这个:

{
    // https://github.com/Microsoft/TypeScript/blob/1f92bacdc81e7ae6706ad8776121e1db986a8b27/lib/lib.d.ts#L1300
    declare global {
        interface Array<T> {
            [n: number]: T | undefined;
        }
    }

    const xs = [1,2,3]
    const x = xs[100]
    x // still number :-(
}

有任何想法吗?

在本地复制lib.d.ts ,比如lib.strict.d.ts ,将索引签名更改为[n: number]: T | undefined; ,将文件包含在您的编译中。 你应该看到预期的效果。

酷,谢谢你。

此处建议修复的问题是它需要分叉并维护一个单独的lib文件。

我想知道这个功能是否足以保证某种开箱即用的选择。

顺便提一下,ES6 集合 ( Map / Set ) 上的get方法的类型签名在Array时返回T | undefined Array / Object索引签名没有。

这是一个有意识的决定。 这段代码是一个错误会很烦人:

var a = [];
for (var i =0; i< a.length; i++) {
    a[i]+=1; // a[i] is possibly undefined
}

要求每个用户都使用!是不合理的。 或写

var a = [];
for (var i =0; i< a.length; i++) {
    if (a[i]) {
        a[i]+=1; // a[i] is possibly undefined
    }
}

对于地图,通常情况并非如此。

同样,对于您的类型,您可以在所有索引签名上指定| undefined ,您将获得预期的行为。 但是对于Array来说是不合理的。 欢迎您 fork 库并进行任何您需要做的更改,但我们目前没有计划更改标准库中的声明。

我不认为添加标志来改变声明的形状是我们会做的事情。

@mhegazy但对于有孔的数组a[i]实际上可能是未定义的:

let a: number[] = []
a[0] = 0
a[5] =0
for (let i = 0; i < a.length; i++) {
  console.log(a[i])
}

输出是:

undefined
undefined
undefined
undefined
0

我们仍然非常怀疑任何人是否会在实践中从这个标志中获得任何好处。 地图和类似地图的东西已经可以在它们的定义站点上选择加入| undefined ,并且在阵列访问上强制执行类似 EULA 的行为似乎并不成功。 我们可能需要大幅改进 CFA 和类型保护以使其变得可口。

如果有人想修改他们的 lib.d.ts 并修复他们自己代码中的所有下游中断,并显示整体差异的样子以表明这具有一些价值主张,我们对这些数据持开放态度。 或者,如果很多人真的很高兴使用 postfix !更多,但还没有足够的机会这样做,那么这个标志将是一个选择。

我们仍然非常怀疑任何人是否会在实践中从这个标志中获得任何好处。 地图和类似地图的东西已经可以在其定义站点上选择加入| undefined

TypeScript 的目标之一不是允许在“编译”时捕获错误,而不是依靠用户记住/知道做一些特定的事情吗? 这似乎与那个目标背道而驰; 要求用户做一些事情以避免崩溃。 许多其他功能也是如此。 如果开发人员总是执行 x,则不需要它们。 TypeScript 的目标(大概)是让工作更轻松并消除这些事情。

我遇到了这个错误,因为我在现有代码上启用了strictNullChecks并且我已经进行了比较,所以我得到了错误。 如果我一直在编写全新的代码,我可能不会意识到这里的问题(类型系统告诉我我总是得到一个值)并以运行时失败告终。 依靠 TS 开发人员记住(或者更糟,甚至知道)他们应该用| undefined声明他们的所有地图,感觉就像 TypeScript 没有做人们真正想要它做的事情。

我们仍然非常怀疑任何人是否会在实践中从这个标志中获得任何好处。 地图和类似地图的东西已经可以选择加入 | 在其定义站点上未定义
TypeScript 的目标之一不是允许在“编译”时捕获错误,而不是依靠用户记住/知道做一些特定的事情吗?

其实目标是:

1) 静态地识别可能是错误的结构。

这里讨论的是错误的可能性(在 TypeScript 团队看来很低)和语言的常见生产可用性。 CFA 的一些早期变化是减少危言耸听或改进 CFA 分析以更智能地确定这些事情。

我认为 TypeScript 团队的问题是,与其争论它的严格正确性,不如提供这种严格性的示例,在通常的用法中,实际上会识别出应该防范的错误。

我在此评论https://github.com/Microsoft/TypeScript/issues/11238#issuecomment -250562397 中进行了更多推理

认为这两种类型在世界上的键的:那些你知道确实有一些目标(安全)的相应属性,那些你知道有没有在某些对象(危险的),相应的属性。

通过编写正确的代码,您可以获得第一种密钥,即“安全”密钥

for (let i = 0; i < arr.length; i++) {
  // arr[i] is T, not T | undefined

或者

for (const k of Object.keys(obj)) {
  // obj[k] is T, not T | undefined

您可以从密钥中获得第二种类型,即“危险”类型,来自用户输入、磁盘中的随机 JSON 文件或某些可能存在但可能不存在的密钥列表。

所以如果你有一个危险类型的键和它的索引,在这里有| undefined会很好。 但该提案不是“将危险的钥匙视为危险”,而是“将所有钥匙,甚至是安全的钥匙,视为危险”。 一旦你开始将安全钥匙视为危险,生活真的很糟糕。 你写的代码像

for (let i = 0; i < arr.length; i++) {
  console.log(arr[i].name);

并且 TypeScript 向你抱怨arr[i]可能是undefined即使嘿,我只是 @#%#ing 测试了它。 现在养成了这样写代码的习惯,感觉很傻:

for (let i = 0; i < arr.length; i++) {
  // TypeScript makes me use ! with my arrays, sad.
  console.log(arr[i]!.name);

或者你可能会写这样的代码:

function doSomething(myObj: T, yourObj: T) {
  for (const k of Object.keys(myObj)) {
    console.log(yourObj[k].name);
  }
}

TypeScript 说“嘿,该索引表达式可能是| undefined ,所以你尽职尽责地修复它,因为你已经看到这个错误 800 次了:

function doSomething(myObj: T, yourObj: T) {
  for (const k of Object.keys(myObj)) {
    console.log(yourObj[k]!.name); // Shut up TypeScript I know what I'm doing
  }
}

但是你没有修复这个错误。 你打算写Object.keys(yourObj) ,或者myObj[k] 。 这是最糟糕的编译器错误,因为它实际上在任何情况下都没有帮助您 - 它只是将相同的仪式应用于每种表达式,而不考虑它是否实际上比任何其他相同形式的表达式更危险。

我想起了旧的“您确定要删除此文件吗?” 对话。 如果对话框出现每一次你试图删除文件时,你会很快学会打del y当你习惯打del ,和你没有删除一些重要的重置前的机会- 对话基线。 相反,如果该对话框仅在您删除未进入回收站的文件时出现,那么现在您就拥有了有意义的安全性。 但是我们不知道(也不可能)您的对象键是否安全,因此显示“您确定要索引该对象吗?” 每次您执行对话框时,都不太可能以比不显示所有内容更好的速度找到错误。

静态地识别可能是错误的构造。

也许这需要修改为“静态识别比其他结构眨眼:. 我想起了当我们遇到基本上是“我使用*当我打算使用/ ,你可以使用 make *警告吗?”

我明白,但索引超出范围是一个真实而普遍的问题; 强迫人们以他们无法做到的方式枚举数组并不是一件坏事。

!修复我实际上也不喜欢 - 如果有人出现并进行更改以使假设现在无效怎么办? 你又回到了原点(编译器应该捕获的东西的潜在运行时失败)。 应该有枚举数组的安全方法,这些方法不依赖于对类型撒谎或使用! (例如,你不能做类似array.forEach(i => console.log(i.name)事情吗?)。

你已经根据代码缩小了类型,所以理论上你不能发现在这些情况下可以安全地缩小类型以删除| undefined ,从而两全其美吗? 我认为,如果您不能轻松地向编译器传达您没有访问有效元素的信息,那么您的保证要么无效,要么将来很容易被意外破坏。

也就是说,我只在一个项目中使用 TS,并且最终将迁移到 Dart,因此它不太可能对我产生任何真正的影响。 我很遗憾软件的总体质量很差,并且有机会帮助消除此处似乎为了方便而被忽略的错误。 我确信类型系统可以变得稳固,并且以不引入这些漏洞的方式解决常见的烦恼。

无论如何,这只是我的 2 美分.. 我不想拖延这个 - 我相信你明白我们来自哪里,你比我更有资格做出决定:-)

我认为有几点需要考虑。 有很多模式可用于在常用的数组上进行迭代,这些模式考虑了元素的数量。 虽然随机访问数组上的索引是一种可能的模式,但实际上这是一种非常罕见的模式,不太可能是静态错误。 虽然有现代迭代方法,但最常见的方法是:

for (let i = 0; i < a.length; i++) {
  const value = a[i];
}

如果您假设备用数组不常见(它们是),那么让value成为| undefined几乎没有帮助。 如果在野外有一个常见的模式,这是有风险的(并且可能是错误的),那么我认为 TypeScript 会听取考虑这一点,但是通常使用的模式,必须再次针对所有值索引访问未定义显然会影响生产力,正如所指出的,如果您处于潜在有用的情况下,可以选择加入。

我认为之前有过关于改进 CFA 的讨论,以便有一种方法来表达值的相互依赖性(例如 Array.prototype.length 与索引值相关),以便可以静态分析索引越界之类的事情。 显然,这是一项重要的工作,涉及各种我不想理解的边缘情况和考虑因素(尽管安德斯很可能会因为这样的事情而冷汗淋漓地醒来)。

所以它变成了一种权衡……如果没有 CFA 改进,90% 的代码会用红鲱鱼复杂化,以捕获可能 10% 的坏代码。 否则,它会投资于主要的 CFA 改进,这可能会导致其自身的稳定性和问题的后果,从而找到不安全的代码。

TypeScript 能做的只有这么多来拯救我们自己。

所有这些重点都集中在数组上,我同意它不太可能成为问题,但是提出的大多数原始问题(如我的)都是关于地图的,我认为常见的情况根本不是始终存在的键?

所有这些重点都集中在数组上,我同意它不太可能成为问题,但是提出的大多数原始问题(如我的)都是关于地图的,我认为常见的情况根本不是始终存在的键?

如果这是您的类型,请将| undefined到索引签名中。 在--noImplicitAny下索引没有索引签名的类型已经是一个错误。
ES6 Map已经用 get 定义为get(key: K): V | undefined;

我重写了数组和映射的所有定义,使索引签名返回| undefined ,从那以后我再也没有后悔过,发现了一些错误,它不会引起任何不适,因为我通过一个手工制作的库间接使用数组来保持检查对于 undefined 或!里面的

如果 TypeScript 可以像 C# 那样控制检查流程(消除索引范围检查以节省一些处理器时间),那就太好了,例如:

declare var values: number[];
for (let index = 0, length = values.length; index< length; index ++) {
   const value = value[index]; // always defined, because index is within array range and only controlled by it
}

(对于那些使用稀疏数组的人 - 用炽热的火焰杀死自己)

至于Object.keys ,它需要一个特殊的类型说allkeysof T让控制流分析做安全的缩小

我认为这将是一个不错的选择,因为现在我们基本上在索引操作的类型上撒谎,并且很容易忘记将| undefined到我的对象类型中。 我认为在我们知道要忽略undefined的情况下添加!将是启用此选项时处理索引操作的好方法。

|undefined放入对象类型定义中(至少)还有另外两个问题:

  • 这意味着您可以undefined
  • 其他操作,如Object.values (或_.values )将要求您处理结果中的undefined

tslint 报告恒定条件的误报警告,因为打字稿返回错误的类型信息(= 缺少| undefined )。
https://github.com/palantir/tslint/issues/2944

在数组索引中缺少| undefined时经常跳过的错误之一是使用这种模式代替find

const array = [ 1, 2, 3 ];
const firstFour = array.filter((x) => (x === 4))[0];
// if there is no `4` in the `array`,
// `firstFour` will be `undefined`, but TypeScript thinks `number` because of the indexer signature.
const array = [ 1, 2, 3 ];
const firstFour = array.find((x) => (x === 4));
// `firstFour` will be correctly typed as `number | undefined` because of the `find` signature.

我肯定会使用这个标志。 是的,旧的for循环使用起来会很烦人,但是我们有!操作符可以在我们知道它被定义时告诉编译器:

for (let i = 0; i < arr.length; i++) {
  foo(arr[i]!)
}

此外,这个问题不是新的,更好的for of循环的问题,甚至还有一个 TSLint 规则prefer-for-of告诉你不要使用旧式的for循环了。

目前我觉得开发人员的类型系统不一致。 array.pop()需要if检查或!断言,但通过[array.length - 1]不需要。 ES6 map.get()需要if检查或!断言,但对象哈希不需要。 @sompylasar的例子也很好。

另一个例子是解构:

const specifier = 'Microsoft/TypeScript'
const [repo, revision] = specifier.split('@') // types of repo and revision are string
console.log('Repo: ' + repo)
console.log('Short rev: ' + revision.slice(0, 7)) // Error: Cannot call function 'slice' on undefined

如果编译器强迫我这样做,我会更喜欢:

const specifier = 'Microsoft/TypeScript'
const [repo, revision] = specifier.split('@') // types of repo and revision are string | undefined
console.log('Repo: ', repo || 'no repo')
console.log('Short rev:', revision ? revision.slice(0, 7) : 'no revision')

这些是我见过的实际错误,它们本来可以被编译器阻止。

Imo 这不应该属于打字文件,而应该是一个类型系统机制 - 当访问任何带有索引签名的东西时,它可以是undefined 。 如果您的逻辑确保它不是,只需使用! 。 否则添加if就可以了。

我认为很多人更喜欢编译器对一些需要的断言严格而不是对未捕获的错误松散。

我真的很想看到添加这个标志。 在我公司的代码库中,数组随机访问是罕见的例外,而for循环是我们通常想用高阶函数重写的代码异味。

@pelotom那么你有什么顾虑(因为你似乎大部分时间都摆脱了麻烦)?

@aleksey-bykov 主要是对象索引签名,广泛出现在第三方库中。 我想访问{ [k: string]: A }上的属性以警告我结果可能未定义。 我只提到了数组索引,因为它被提出来说明为什么标志太烦人而无法使用。

你知道你可以完全按照你想要的方式重写它们吗? (给一些额外的工作)

是的,我可以为他们重写每个人的类型,或者我可以打开编译器标志😜

继续玩船长 O...:你今天可以重写你的lib.d.ts并成为更多健全代码库的快乐拥有者,或者你可以等待未来 N 年的旗帜

@aleksey-bykov 如何通过重写lib.d.ts

declare type Keyed<T> = { [key: string]: T | undefined; }

然后在lib.es2015.core.d.tsArray定义中,替换

[n: number]: T;

[n: number]: T | undefined;

@aleksey-bykov 也许你错过了我说我不关心数组的部分。 我关心第三方库在哪里声明了{ [k: string]: T }类型的东西,我想访问这样的对象来返回可能未定义的东西。 没有办法通过简单地编辑lib.d.ts来实现这一点; 它需要更改相关库的签名。

您可以控制第 3 方定义文件吗? 如果是这样,您可以修复它们

现在我们回到

是的,我可以为他们重写每个人的类型,或者我可以打开编译器标志😜

时间是一个扁平的圆圈。

别傻了,你不会使用“每个人的打字”吗? 对于一个典型的项目来说,这实际上是一天的最大工作量,已经完成了

是的,我必须一直编辑别人的打字,我希望少做。

你会在 N 年后,也许,现在你可以受苦或勇敢

感谢您提供令人难以置信的建设性意见👍

对此的建设性意见如下,这个问题需要结束,因为:
一种。 [x]可以为undefined决定由开发人员决定

  • 让他们像以前一样把这一切都记在脑子里
  • 或按照建议更改lib.d.ts3rd-party.d.ts

湾或者它需要特殊的语法/类型/流分析/N年才能减轻在#a中可以轻松完成的事情

问题是 (b) 的提案,除了没有提出新的语法之外,它只是一个编译器标志。

归根结底, { [x: string]: {} }类型几乎总是一个谎言; 除非使用Proxy ,否则没有对象可以拥有无​​限数量的属性,更不用说每个可能的字符串了。 建议是有一个编译器标志来识别这一点。 可能是因为获得的东西很难实现; 我会把这个电话留给实现者。

关键是两者都不是

  • T | undefined
  • 也没有T

适用于一般情况

为了使其适用于一般情况,您需要将有关值的存在的信息编码到需要依赖类型系统的容器类型中......这本身并不是一件坏事:)但可能像今天所有当前的打字稿类型系统一样复杂,为了……为您节省一些编辑?

T | undefined对于一般情况是正确的,原因我刚刚给出。 将忽略你关于依赖类型的荒谬胡说八道,祝你有美好的一天。

你可以随心所欲地忽略我,但T | undefined超出了

declare var items: number[];
for (var index = 0; index < items.length; index ++) {
   void items[index];
}

我宁愿在默认情况下有T | undefined并告诉编译器indexitems的数字索引范围,因此如果应用于items时不会出现边界for / while循环形状,编译器可以自动推断; 在复杂的情况下,抱歉,可能会有undefined s。 是的,基于值的类型很适合这里; 文字字符串类型是如此有用,为什么没有文字布尔值和数字以及范围/范围集类型? 就 TypeScript 而言,它试图涵盖可以用 JavaScript 表达的所有内容(与限制它的 Elm 相比)。

对于一个典型的项目来说,这实际上是一天的最大工作量,已经完成了

@aleksey-bykov,好奇你在改变之后的体验是什么? 你多久必须使用! ? 您发现编译器标记实际错误的频率如何?

@mhegazy老实说,我没有注意到从TT | undefined太大区别,我也没有发现任何错误,我想我的问题是我通过保留!实用程序函数处理数组

https://github.com/aleksey-bykov/basic/blob/master/array.ts

我可以在哪个lib文件中找到对象的索引类型定义? 我已经找到并更新了Array[n: number]: T[n: number]: T | undefined 。 现在我想对对象做同样的事情。

具有索引签名的对象没有标准接口(如数组的Array ),您需要在代码中针对每种情况查找确切的定义并修复它们

您需要在代码中为每种情况查找确切的定义并修复它们

直接键查找怎么样? 例如

const xs = { foo: 'bar' }
xs['foo']

有没有办法在这里强制执行T | undefined而不是T ? 目前,我在我的代码库中到处使用这些帮助程序,作为对数组和对象进行索引查找的类型安全替代方法:

// TS doesn't return the correct type for array and object index signatures. It returns `T` instead
// of `T | undefined`. These helpers give us the correct type.
// https://github.com/Microsoft/TypeScript/issues/13778
export const getIndex = function<X> (index: number, xs: X[]): X | undefined {
  return xs[index];
};
export const getKeyInMap = function<X> (key: string, xs: { [key: string]: X }): X | undefined {
  return xs[key];
};

@mhegazy在撰写本文时,我正在修复https://unsplash.com上的一个生产错误,该错误可能会被更严格的索引签名类型捕获。

我明白了,考虑映射类型运算符:

const xs = { foo: 'bar' };
type EachUndefined<T> = { [P in keyof T]: T[P] | undefined; }
const xu : EachUndefined<typeof xs> = xs;
xu.foo; // <-- string | undefined

如果像--strictArrayIndex这样的标志不是一个选项,因为标志不是为了更改lib.d.ts文件而设计的。 也许你们可以发布严格版本的lib.d.ts文件,例如“ lib": ['strict-es6']

它可以包含多项改进,而不仅仅是严格的数组索引。 例如, Object.keys

interface ObjectConstructor {
    // ...
    keys(o: {}): string[];
}

可能:

interface ObjectConstructor {
    // ...
    keys<T>(o: T): (keyof T)[];
}

今日SBS更新:我们互相吼了30分钟,什么也没发生。 🤷‍♂️

@RyanCavanaugh出于好奇,什么是 SBS?

@radix “建议积压日志”

这很有趣,因为解决方案很明显:
TT | undefined都是错误的,唯一正确的方法是让索引变量知道其容器的容量,通过从集合中选取或将其包含在已知的数字范围内

@RyanCavanaugh我一直在思考这个问题,看起来以下技巧可以覆盖 87% 的情况:

  • 如果在for (HERE; ...)声明索引,则values[index]给出T for (HERE; ...)
  • values[somethingElse]T | undefined之外声明的所有变量提供for

@aleksey-bykov 我们讨论了一些更聪明的事情 - “ arr[index]index < arr.length测试的实际类型保护机制可能存在。但这对你pop的情况没有帮助

不要暗示 Aleksey 会改变数组

我最近将一个应用程序从 Elm 移植到了 Typescript,并且索引操作输入错误可能是我遇到的最大错误来源之一,启用了所有最严格的 TS 设置(还有像this这样的未绑定的东西) )。

  • 您无法跟踪容器的突变
  • 您无法跟踪索引操作

说了这么多,你几乎不能保证,如果是这样,如果一个典型的用例是通过< array.length内的所有元素进行愚蠢迭代,你为什么还要尝试

就像我平时说的

  • 如果有错误,它很可能源自for语句的子句中的某处:初始化、增量、停止条件,因此不是您可以验证的,因为它需要图灵完整的类型检查
  • for子句之外通常没有错误的地方

所以只要以某种方式声明了索引(我们不能自信地说它),相信索引是正确的循环体是合理的

但这对您在循环中间弹出或将数组传递给从中删除元素的函数的情况没有帮助

这似乎是一个非常弱的反对意见。 那些案例已经坏了——以它们为借口不修复其他案例是没有意义的。 这里没有人要求正确处理这两种情况中的任何一种,也不会要求它 100% 正确。 我们只想要在非常常见的情况下准确的类型。 TypeScript 中有很多情况没有得到完美的处理; 如果你在做一些奇怪的事情,类型可能是错误的。 在这种情况下,我们没有做任何奇怪

为正确的代码添加仪式

我很想看到一个代码示例,说明这是“添加正确代码的仪式”。 据我了解,数组上的基本 for 循环很容易检测/缩小。 什么不是简单的 for 循环的现实世界代码

在不捕捉错误的同时

已经有工作代码因此无法编译和代码在运行时抛出的例子,因为类型误导了开发人员。 说支持这一点的价值为零是无稽之谈。

为什么我们不能保持简单,而不是在旧式 for 循环周围添加 _any_ 魔术行为。 你总是可以使用!来让事情工作。 如果您的代码库充满旧式for循环,请不要使用该标志。 我使用过的所有现代代码库都使用forEachfor of来迭代数组,这些代码库将受益于额外的类型安全。

我们没有做任何奇怪的事情并且类型是错误的。

对我来说,这一段读起来像是没有这个功能的一个很好的理由。 越界访问数组是一件很奇怪的事情; 如果您正在做一件不常见的事情(OOBing),这些类型只会是“错误的”。 从数组中读取的绝大多数代码都是在边界内进行的; 在这些情况下,通过此参数包含undefined将是“错误的”。

... 无法编译的工作代码

我不知道任何 - 你能具体指出他们吗?

为什么我们不能保持简单并且根本不围绕旧式 for 循环添加任何魔术行为。 您可以随时使用! 使事情发挥作用。

这与 TSLint 规则(即“所有数组访问表达式必须具有尾随! ”)之间的有用区别是什么?

@RyanCavanaugh我的假设是 TSLint 规则无法缩小类型或使用控制流类型分析(例如将访问包装在if ,抛出异常, return ing 或continue ing 如果未设置,等等)。 它不仅与数组访问表达式有关,还与解构有关。 https://github.com/Microsoft/TypeScript/issues/13778#issuecomment -336265143 中的示例的实现如何? 为了强制执行! ,它本质上必须创建一个表来跟踪这些变量的类型,因为它可能是undefined 。 对我来说,这听起来像是类型检查器应该做的事情,而不是短绒。

@瑞安卡瓦诺

对我来说,这一段读起来像是没有这个功能的一个很好的理由。 越界访问数组是一件很奇怪的事情; 如果您正在做一件不常见的事情(OOBing),这些类型只会是“错误的”。

... 无法编译的工作代码

我不知道任何 - 你能具体指出他们吗?

在我的情况下,它不是一个数组,但它作为一个骗局被关闭,所以我认为这个问题也应该涵盖这些。 有关我的原始问题,请参阅 #11186。 我正在将文件解析为地图,然后根据undefined 。 IIRC 我在比较中得到错误,它们不能是未定义的,即使它们可以(并且曾经是)。

总是允许将某些东西与undefined

这是一种耻辱

const canWeDoIt = null === undefined; // yes we can!

总是允许将某些东西与undefined

这么久了,恐怕我不记得到底是什么错误了。 对于运行良好的代码(没有strictNullChecks ),我肯定有一个错误,它引导我进行导致上述情况的测试。

如果我有时间,我会看看我是否能再次弄清楚它到底是什么。 这肯定与这种打字有关。

大多数讨论都集中在数组上,但根据我的经验,对象访问是此功能最有用的地方。 例如,我的代码库中的这个(简化)示例看起来合理并且编译良好,但绝对是一个错误:

export type Chooser = (context?: Context) => number | string;
export interface Choices {
    [choice: number]: Struct;
    [choice: string]: Struct;
}

export const Branch = (chooser: Chooser, choices: Choices, context?: Context): Struct => {
    return choices[chooser(context)];  // Could be undefined
}

关于对象并简单地将签名更改为包含| undefined@types/nodeprocess.env

    export interface ProcessEnv {
        [key: string]: string | undefined;
    }

但它根本不允许缩小类型:

process.env.SOME_CONFIG && JSON.parse(process.env.SOME_CONFIG)

[ts]
Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
  Type 'undefined' is not assignable to type 'string'.

@felixfbecker

我相信这与此错误有关: https :

您可以通过将 env var 分配给一个变量来解决它,然后保护它:

const foo = process.env.SOME_CONFIG
foo && JSON.parse(foo);

在某些节点类型中添加| undefined是为了允许扩充这些接口,以方便使用常用的属性来完成代码。 EA

interface ProcessEnv {
  foo?: string;
  bar?: string;
}

如果索引签名中没有| undefined ,TSC 会抱怨Property 'foo' of type 'string | undefined' is not assignable to string index type 'string'.

我知道如果您必须使用具有额外| undefined和没有它的类型,这会产生成本。

如果我们能摆脱这个,那就太好了。

我正在尝试收集我对这个问题的想法。

对象在 JavaScript 中是混合的。 它们可用于两个目的

  • 字典又名地图,其中的键是未知的
  • 记录,其中的密钥是已知的

对于用作记录的对象,不需要使用索引签名。 相反,已知键是在接口类型上定义的。 有效的键查找返回相应的类型,无效的键查找将回退到索引签名。 如果没有定义索引签名(对于用作记录的对象不应该有),并且启用了noImplicitAny ,这将根据需要出错。

对于用作字典(又名映射)和数组的对象,使用索引签名,我们可以选择在值类型中包含| undefined 。 例如, { [key: index]: string | undefined } 。 所有键查找都是有效的(因为键在编译时未知),并且所有键都返回相同的类型(在本例中T | undefined )。

看到索引签名应该只用于字典对象模式和数组,希望 TypeScript 应该在索引签名值类型中强制执行| undefined :如果键未知,键查找可能返回undefined

有一些很好的例子,如果没有这个,可能看起来不可见的错误,例如Array.prototype.find返回undefined ,或键查找,例如array[0]返回undefined 。 (解构只是键查找的语法糖。)可以编写像getKey这样的函数来纠正返回类型,但是我们必须依靠纪律来强制跨代码库使用这些函数。

如果我理解正确,那么问题就变成了对字典对象和数组的反射,这样在映射字典对象或数组的键时,键查找被认为是有效的,即不会返回undefined 。 在这种情况下,值类型不希望包含undefined 。 可以使用控制流分析来解决这个问题。

这是悬而未决的问题,还是我误解了?

我没有提到哪些用例和问题?

将 |undefined 放入对象类型定义中(至少)还有另外两个问题:

  • 这意味着您可以将 undefined 分配给这些对象,这绝对不是故意的
  • 其他操作,如 Object.values(或 _.values)将要求您处理结果中的 undefined

我认为这是非常重要的一点。

目前,我正在试验以下方法:

定义const safelyAccessProperty = <T, K extends keyof T>(object: T, key: K): T[K] | undefined => object[key];

然后访问类似safelyAccessProperty(myObject, myKey)属性,而不是myObject[myKey]

@plul好收获。 目前讨论的重点是读操作,但索引器类型定义实际上是两方面的,添加| undefined将允许写入undefined值。

您正在试验的safelyAccessProperty函数(@OliverJAsh在上面提到的getKey )需要纪律和/或 linter 规则来禁止对所有数组和对象进行索引操作。

如果在所有数组和对象实例(提供索引器操作的每种类型)上都提供了该函数,则可以使其具有可扩展性,例如在 C++ 中std::vector具有.at() ,它会在运行时为 OOB 访问抛出异常,以及未经检查的[]运算符,在最佳情况下会在 OOB 访问时与 SEGFAULT 崩溃,在最坏情况下会损坏内存。

我认为 OOB 访问问题在 TypeScript/JavaScript 中无法单独在类型定义级别解决,并且如果启用了此严格功能,则需要语言支持来限制潜在危险的索引器操作。

索引器的双重性质可以建模为具有getset操作作为函数的属性,但这对于所有现有索引器类型定义来说将是一个重大变化。

一个看起来很有希望的想法:如果您可以在定义索引签名时使用?:来指示您希望在正常使用下有时会丢失值怎么办? 它就像上面提到的| undefined ,但没有尴尬的缺点。 它需要禁止显式undefined值,我想这与通常的?:

它看起来像这样:

type NewWay = {[key: string]?: string};
const n: NewWay = {};

// Has type string | undefined
n['foo']

// Has type Array<string>
Object.values(n)

// Doesn't work
n['foo'] = undefined;

// Works
delete n['foo'];

与之前的| undefined方法相比:

type OldWay = {[key: string]: string | undefined};
const o: OldWay = {};

// Has type string | undefined
o['foo']

// Has type Array<string | undefined>
Object.values(o)

// Works
o['foo'] = undefined;

// Works
delete o['foo'];

我来到这里是因为拒绝在上面的 DT PR 中添加| undefined ,因为它会破坏该 API 的所有现有用户 - 这是否可以更好地视为允许用户选择他们想要的挑剔程度,而不是比图书馆?


我会注意到可选属性也添加了| undefined ,这让我咬了几次 - 本质上 TS 不区分丢失的属性和设置为未定义的属性。 我只是希望{ foo?: T, bar?: T }被视为与{ [name: 'foo' | 'bar']: T } ,无论哪种方式(另请参阅上面的process.env评论)


TS 是否反对在数字和字符串索引器上破坏对称性?

foo[bar] && foo[bar].baz()是一个很常见的 JS 模式,在 TS 不支持的时候感觉很笨拙(意思是提醒你不加| undefined ,加了就警告如果您这样做,显然不需要)。


关于在迭代过程中改变数组打破了守卫表达式的保证,其他守卫也可以这样做:

class Foo {
    foo: string | number = 123

    bar() {
        this.foo = 'bar'
    }

    broken() {
        if (typeof this.foo === 'number') {
            this.bar();
            this.foo.toLowerCase(); // right, but type error
            this.foo.toExponential(); // wrong, but typechecks
        }
    }
}

但我想在实际代码中,旧循环改变迭代对象的可能性要小得多。

很明显,这个功能是有需求的。 我真的希望 TS 团队能找到一些解决方案。 不仅仅是将| undefined到索引器,因为它有自己的问题(已经提到),而是通过更“聪明”的方式(读取返回T|undefined ,写入需要T ,好的编译器检查for循环等。也提到了好的提议。)

当我们以非平凡的方式对数组进行变异和处理时,我们对运行时错误没有问题,很难通过编译器方式进行验证。 我们只想在大多数情况下进行错误检查,有时使用!也可以。

话虽如此,如果实现这种更严格的数组处理,现在使用 #24897 可以在使用常量检查数组长度时实现良好的类型缩小。 我们可以将数组缩小到带有剩余元素的元组。

let arr!: string[];
if (arr.length == 3) {
  //arr is of type [string, string, string]
}

if (arr.length > 3) {
  //arr is of type [string, string, string, string, ...string[]]
}

if (arr.length) {
  //arr is of type [string, ...string[]]
}

if (arr.length < 3) {
  //arr is of type [string?, string?, string?]
  if (arr.length > 0) {
    //arr is of type [string, string?, string?]
  }
}

当您按常量索引或解构数组时,它会很有用。

let someNumber = 55;
if (arr.length) {
  let el1 = arr[0]; //string
  let el2 = arr[1]; //string | undefined
  let el3 = arr[someNumber]; //string | undefined
}

if(arr.length >= 3){
    let [el1, el2, el3, el4] = arr;
    //el1, el2, el3 are string
    // el4 is string | undefined    
}

if (arr.length == 2){
    let [el1, el2, el3] = arr; //compiler error: "Tuple type '[string, string]' with length '2' cannot be assigned to tuple with length '3'.",
}

另一个问题是我们将如何处理大数字,例如:

if(arr.length >= 99999){
    // arr is [string, string, ... , string, ...string[]]
}

我们无法在 IDE 或编译器消息中显示这个巨大元组的类型。

我想我们可以有一些语法来表示“所有项目具有相同类型的特定长度的元组”。 因此,例如 1000 个字符串的元组是string[10000]并且上面示例中缩小数组的类型可能是[...string[99999], ...string[]]

另一个问题是编译器基础设施现在是否可以支持这么大的元组,如果不能的话,这会有多难。

对象

我总是想要[key: string (or number, symbol)]: V | undefined的索引类型,只是有时我会忘记undefined情况。 每当开发人员必须明确告诉编译器“相信我,这是真正的那种东西”时,您就知道它是不安全的。
正确地(严格地)键入Map.get几乎没有意义,但不知何故,普通对象可以获得免费通行证。
尽管如此,这在用户领域很容易修复,所以还不错。 无论如何,我没有解决方案。

数组

也许我遗漏了一些东西,但似乎“你几乎从不以不安全的方式访问数组”的论点可以双向进行,尤其是使用新的编译器标志时。

我倾向于认为越来越多的人遵循以下两个最佳实践:

  • 使用函数式本地方法或库来迭代或转换数组。 这里没有支架访问。
  • 不要就地改变数组

考虑到这一点,您需要低级括号访问逻辑的唯一剩余和罕见情况将真正受益于类型安全。

这似乎很简单,我认为在本地复制粘贴整个 lib.d.ts 不是一种可以接受的解决方法。

当我们显式索引数组/对象时,例如获取数组的第一项 ( array[0] ),我们希望结果包含undefined

这可以通过将undefined到索引签名来实现。

但是,如果我理解正确,在索引签名中包含undefined的问题是,在映射数组或对象时,值将包含undefined即使它们不知道是undefined (否则我们不会映射它们)。

索引类型/签名用于索引查找(例如array[0] )和映射(例如for循环和Array.prototype.map ),但是对于这些情况,我们需要不同的类型。

这不是Map的问题,因为Map.get是一个函数,因此它的返回类型可以与内部值类型分开,不像索引到不是函数的数组/对象,因此直接使用索引签名。

那么,问题就变成了:我们如何才能同时满足这两种情况?

// Manually adding `undefined` to the index signature
declare const array: (number | undefined)[];

const first = array[0]; // number | undefined, as desired :-)
type IndexValue = typeof array[0]; // number | undefined, as desired! :-)

array.map(x => {
  x // number | undefined, not desired! :-(
})

提议

编译器选项,它对待索引查找(例如array[0]类似)如何Set.getMap.get被键入,通过包括undefined索引中的值的类型(未索引签名本身)。 实际的索引签名本身不包括undefined ,因此映射功能不会受到影响。

例子:

declare const array: number[];

// The compiler option would include `undefined` in the index value type
const first = array[0]; // number | undefined, as desired :-)
type IndexValue = typeof array[0]; // number | undefined, as desired :-)

array.map(x => {
  x // number, as desired :-)
})

然而,这不会解决使用for循环遍历数组/对象的情况,因为该技术使用索引查找。

for (let i = 0; i < array.length; i++) {
  const x = array[i];
  x; // number | undefined, not desired! :-(
}

对于我和我怀疑许多其他人,这是可以接受的,因为没有使用for循环,而是更喜欢使用功能样式,例如Array.prototype.map 。 如果我们确实必须使用它们,我们会很高兴使用此处建议的编译器选项以及非空断言运算符。

for (let i = 0; i < array.length; i++) {
  const x = array[i]!;
  x; // number, as desired :-)
}

我们还可以提供一种选择加入或选择退出的方法,例如使用一些语法来装饰索引签名(请原谅我为示例提出的模棱两可的语法)。 这种语法只是一种表明我们想要索引查找的行为的方式。

选择退出(编译器选项默认启用,需要时选择退出):

declare const array: { [index: number]!!: string };

declare const dictionary: { [index: string]!!: string }

选择加入(没有编译器选项,只需在需要时选择加入):

declare const array: { [index: string]!!: string };

declare const dictionary: { [index: string]??: string }

我没有读过这个问题或利弊、各种建议等(在反复惊讶于数组/对象访问没有通过严格的空检查一致处理后才在 Google 中找到它),但我有一个相关建议:除非特别覆盖,否则使数组类型推断尽可能严格的选项。

例如:

const balls = [1, 2 ,3];

默认情况下, balls将被视为[number, number, number] 。 这可以通过写来覆盖:

const balls: number[] = [1, 2 ,3];

此外,元组元素访问将通过严格的空检查一致地处理。 令我惊讶的是,在以下示例中,即使启用了严格的空检查, n目前也被推断为number

const balls: [number, number, number] = [1, 2 ,3];
const n = balls[100];

我还希望元组类型定义中不存在诸如.push类的数组变异方法,因为此类方法会将运行时类型更改为与编译时类型不一致。

@buu700欢迎使用 TypeScript 3.0: https: //blogs.msdn.microsoft.com/typescript/2018/07/30/annoucing-typescript-3-0/#richer -tuple-types

好的! 嗯,这是有趣的时机。 我已经打开了发布公告,但还没有通读; 只是在遇到我需要做一些奇怪的转换 ( (<(T|undefined)[]> arr).slice(-1)[0] ) 以使 TypeScript (2.9) 做我想做的事情后才来到这里的。

只是想把事情带回到这个建议: https :

这将解决我遇到的索引类型的问题。 如果它是默认设置就好了,但我知道这会破坏现实世界中的很多东西。

@mhegazy @RyanCavanaugh对我的提议有什么想法吗? https://github.com/Microsoft/TypeScript/issues/13778#issuecomment -406316164

对我来说,有一个简单的解决方案。 启用检查这一点的标志。 然后,而不是:

const array = [1, 2, 3];
for (var i =0; i< array.length; i++) {
    array[i]+=1; // array[i] is possibly undefined
}

你做:

const array = [1, 2, 3];
array.forEach((value, i) => array[i] = value + 1);

然后,在进行随机索引访问时,您需要检查结果是否未定义,而不是在迭代枚举集合时检查。

我仍然认为这值得有一个悬而未决的问题。

作为一名刚接触 TypeScript 的程序员,我发现在严格模式下索引对象的情况很不直观。 我原以为查找的结果是T | undefined ,类似于Map.get 。 无论如何,我最近刚遇到这个问题,并在图书馆中打开了一个问题:

screepers/typed-screeps#107

我现在可能要关闭它,因为似乎没有好的解决方案。 我想我将通过使用一个小实用函数来尝试“选择加入”到T | undefined

export function lookup<T>(map: {[index: string]: T}, index: string): T|undefined {
  return map[index];
}

这里有一些建议将T | undefined显式设置为对象索引操作返回类型,但这似乎不起作用:

const obj: {[key: string]: number | undefined} = {
  "a": 1,
  "b": 2,
};

const test = obj["c"]; // const test: number

这是 VSCode 版本 1.31.1

@yawaramin确保您在tsconfig.json strictNullChecks启用了strict标志启用)

如果您的用例需要对未知长度的数组进行任意索引,我认为值得明确添加 undefined (如果只是为了“记录”这种不安全性)。

const words = ... // some string[] that could be empty
const x = words[0] as string | undefined
console.log(x.length) // TS error

元组适用于已知长度的小数组。 也许我们可以用string[5]作为[string, string, string, string, string]的简写?

非常赞成将其作为一种选择。 这是类型系统中的一个明显漏洞,尤其是在启用strictNullChecks标志时。 JS 中一直使用普通对象作为映射,因此 TypeScript 应该支持该用例。

使用函数参数的数组析构遇到这个问题:

function foo([first]: string[]) { /* ... */ }

在这里,我希望astring | undefined但它只是string ,除非我这样做

function foo([first]: (string | undefined)[]) { /* ... */ }

我认为我们的代码库中没有一个for循环,所以我很乐意在我的 tsconfig 的编译器选项(可以命名为strictIndexSignatures )中添加一个标志来切换这种行为对于我们的项目。

这就是我一直在解决这个问题的方式: https :

很好的解决方法,我真的希望 TypeScript 团队能够解决这个问题。

当程序员在书面代码中做出假设并且编译器无法推断出它是保存的时,除非沉默恕我直言,否则这应该导致编译器错误。

这种行为与新的可选链运算符结合使用也非常有用。

我今天在使用Object.entries()遇到了将| undefined与地图一起使用的问题。

我有一个由{[key: string]: string[]}很好地描述的索引类型,明显的警告是不是每个可能的字符串键都在索引中表示。 但是,我写了一个错误,TS 在尝试使用从索引中查找的值时没有发现该错误; 没有处理未定义的情况。

所以我按照建议将其更改为{[key: string]: string[] | undefined} ,但是现在这导致了我使用Object.entries() 。 TypeScript 现在假设(合理地,基于类型规范)索引可能具有指定了 undefined 值的键,因此假设对其调用Object.entries()的结果可能包含未定义值。

然而,我知道这是不可能的; 我应该得到undefined结果的唯一一次是查找一个不存在的键,并且在使用Object.entries()时不会列出该键。 所以为了让 TypeScript 开心,我要么编写没有真正存在理由的代码,要么覆盖警告,我不想这样做。

@RyanCavanaugh ,我假设对此

(那里的例子对我来说仍然有些令人信服,但是这个线程已经提出了所有论点,另一个评论不会改变任何东西)

如果有人想修改他们的 lib.d.ts 并修复他们自己代码中的所有下游中断,并显示整体差异的样子以表明这具有一些价值主张,我们对这些数据持开放态度。

@RyanCavanaugh下面是一些从我的代码库,其中一些运行时的崩溃是沉睡的案件。 请注意,我已经遇到过因此导致生产崩溃的情况,需要发布修补程序。

src={savedAdsItem.advertImageList.advertImage[0].mainImageUrl || undefined}
return advert.advertImageList.advertImage.length ? advert.advertImageList.advertImage[0].mainImageUrl : ''
birthYear: profileData.birthYear !== null ? profileData.birthYear : allowedYears[0].value,
upsellingsList.upsellingProducts[0].upsellingProducts[0].selected = true
const latitude = parseFloat(coordinates.split(',')[0])
const advert = Object.values(actionToConfirm.selectedItems)[0]
await dispatch(deactivateMyAd(advert))

在这种情况下,它会很烦人,因为ArticleIDs extends articleNames[] undefined在结果值中包含ReadonlyArray<articleNames>而不是articleNames[]轻松修复。

export enum articleNames {
    WEB_AGB = 'web_agb',
    TERMS_OF_USE = 'web_terms-of-use',
}
export const getMultipleArticles = async <ArticleIDs extends articleNames[], ArticleMap = { [key in ArticleIDs[number]]: CmsArticle }>(ids: ArticleIDs): Promise<ArticleMap> => {...}

总而言之,我真的很想拥有这种额外的类型安全来防止可能的运行时崩溃。

我在这个评论#11238(评论)中更深入地推理

想想世界上有两种类型的键:那些你知道_确实在某个对象中具有相应属性的键(安全),那些你_不知道_在某个对象中具有相应属性的键(危险)。

通过编写正确的代码,您可以获得第一种密钥,即“安全”密钥

for (let i = 0; i < arr.length; i++) {
  // arr[i] is T, not T | undefined

或者

for (const k of Object.keys(obj)) {
  // obj[k] is T, not T | undefined

您可以从密钥中获得第二种类型,即“危险”类型,来自用户输入、磁盘中的随机 JSON 文件或某些可能存在但可能不存在的密钥列表。

所以如果你有一个危险类型的键和它的索引,在这里有| undefined会很好。 但该提议不是“将 _dangerous_ 键视为危险”,而是“将 _所有_ 键,甚至是安全的键视为危险”。 一旦你开始将安全钥匙视为危险,生活真的很糟糕。 你写的代码像

for (let i = 0; i < arr.length; i++) {
  console.log(arr[i].name);

并且 TypeScript 向你抱怨arr[i]可能是undefined ,尽管 _hey 看起来我只是 @#%#ing 测试了它_。 现在养成了这样写代码的习惯,感觉很傻:

for (let i = 0; i < arr.length; i++) {
  // TypeScript makes me use ! with my arrays, sad.
  console.log(arr[i]!.name);

或者你可能会写这样的代码:

function doSomething(myObj: T, yourObj: T) {
  for (const k of Object.keys(myObj)) {
    console.log(yourObj[k].name);
  }
}

TypeScript 说“嘿,该索引表达式可能是| undefined ,所以你尽职尽责地修复它,因为你已经看到这个错误 800 次了:

function doSomething(myObj: T, yourObj: T) {
  for (const k of Object.keys(myObj)) {
    console.log(yourObj[k]!.name); // Shut up TypeScript I know what I'm doing
  }
}

但是你没有修复这个错误。 你打算写Object.keys(yourObj) ,或者myObj[k] 。 这是最糟糕的编译器错误,因为它实际上在任何情况下都没有帮助您 - 它只是将相同的仪式应用于每种表达式,而不考虑它是否实际上比任何其他相同形式的表达式更危险。

我想起了旧的“您确定要删除此文件吗?” 对话。 如果对话框出现每一次你试图删除文件时,你会很快学会打del y当你习惯打del ,和你没有删除一些重要的重置前的机会- 对话基线。 相反,如果该对话框仅在您删除未进入回收站的文件时出现,那么现在您就拥有了有意义的安全性。 但是我们不知道(也不知道我们)您的对象键是否安全,因此显示“您确定要索引该对象吗?” 每次您执行对话框时,都不太可能以比不显示所有内容更好的速度找到错误。

我确实同意删除文件对话框的类比,但是我认为这个类比也可以扩展到强制用户检查可能未定义或为空的内容,所以这个解释没有意义,因为如果这个解释是真的, strictNullChecks选项将引发相同的行为,例如使用document.getElementById从 DOM 获取一些元素。

但事实并非如此,许多 TypeScript 用户希望编译器提高此类代码的标记,以便可以适当地处理这些边缘情况,而不是抛出非常难以追踪的Cannot access property X of undefined错误。

最后,我希望这些特性可以作为额外的 TypeScript 编译器选项来实现,因为这就是用户想要使用 TypeScript 的原因,他们想要警告危险代码。

谈论错误地访问数组或对象不太可能发生,你有任何数据来支持这个说法吗? 还是只是基于任意的直觉?

for (let i = 0; i < arr.length; i++) {
  console.log(arr[i].name);

可以改进 TypeScript 的基于控制流的类型分析,以识别这种情况是安全的,并且不需要! 。 如果人类可以推断出它是安全的,那么编译器也可以。
我知道这在编译器中实现可能并不容易。

可以改进 TypeScript 的基于控制流的类型分析,以识别这种情况是安全的

实在不行。

declare function someFunc(arr: number[], i: number): void;
let arr = [1, 2, 3, 4];
for (let i = 0; i < arr.length; i++) {
  someFunc(arr, arr[i]);
}

这个函数是否在第二次通过循环时将undefined传递给someFunc ,或者不是? 我可以在someFunc中写很多东西,这会导致undefined稍后出现。

那这个呢?

declare function someFunc(arr: number[], i: number): void;
let arr = [1, 2, 3, 4];
let alias = arr;
for (let i = 0; i < arr.length; i++) {
  someFunc(alias, arr[i]);
}

@fabb让我再举一个例子:

``
$节点

常量 arr = []
不明确的
arr[7] = 7
7
阿尔
[ <7 个空物品>, 7 ]
for (让 i = 0; i < arr.length; i++) {
... console.log(arr[i])
... }
不明确的
不明确的
不明确的
不明确的
不明确的
不明确的
不明确的
7
未定义```

@RyanCavanaugh既然你来了,如何推断item :: Tarr :: T[]for (const item of arr) ... ,否则推断arr[i] :: T | undefined使用某些时候--strict-index ? 我关心的情况如何, obj[key] :: V | undefinedObject.values(obj) :: V[]obj :: { [key: string]: V }

@yawaramin如果您使用的是稀疏数组,则 Typescript 已经没有做正确的事情。 --strict-index标志可以解决这个问题。 这编译:

const arr = []
arr[7] = 7

for (let i = 0; i < arr.length; i++) {
    console.log(Math.sqrt(arr[i]));
}

@RyanCavanaugh有一个非常常见的例子,我可以向您展示用户容易错误访问数组的地方。

const getBlock = (unitNumber: string): string => unitNumber.split('-')[0]

上面的代码不应该在strictNullChecks下优雅地通过编译器检查,因为getBlock某些用途将返回未定义的,例如getBlock('hello') ,在这种情况下,我非常希望编译 raise 标志,以便我可以优雅地处理未定义的情况而不会爆炸我的应用程序。

这也适用于许多常见的习惯用法,例如使用arr.slice(-1)[0]访问数组的最后一个元素,访问第一个元素arr[0]等。

最终,我希望 TypeScript 会因为这些错误而惹恼我,而不必处理爆炸的应用程序。

这个函数是否在第二次通过循环时将 undefined 传递给 someFunc ,或者不是? 有很多我可以在 someFunc 中编写的东西会导致稍后出现 undefined 。

@RyanCavanaugh是的,JavaScript 不适合不变性。 在这种情况下,应该需要!ReadonlyArray数组: someFunc(arr: ReadonlyArray<number>, i: number)

@yawaramin对于稀疏数组,元素类型可能需要包含undefined除非 TypeScript 可以推断出它像元组一样使用。 在@danielnixon链接到的代码(https://github.com/microsoft/TypeScript/issues/13778#issuecomment-536248028)中,元组也被特殊对待,并且在返回的元素类型中不包含undefined因为编译器保证只访问集合索引。

这是一个有意识的决定。 这段代码是一个错误会很烦人:

var a = [];
for (var i =0; i< a.length; i++) {
    a[i]+=1; // a[i] is possibly undefined
}

嘿,我认得那个语法; 我可能每年写一次这样的循环!

我发现在我实际对数组进行索引的大多数情况下,我实际上想在之后检查undefined

担心使这种特殊情况更难处理似乎有些夸大其词。 如果您只想迭代元素,您可以使用for .. of 。 如果出于某种原因需要元素索引,请使用forEach或迭代entries 。 一般来说,你真的需要一个基于索引的 for 循环是非常罕见的。

如果有更好的理由来说明为什么想要保持现状,我希望看到它们,但无论如何:这是系统中的不一致,并且有一个标志来修复它似乎会受到许多人的高度赞赏。

大家好,我觉得这里的大部分讨论都是
有。 包所有者已经非常清楚他们的推理和
已经考虑了大多数这些论点。 如果他们打算解决
这个,我相信他们会公开的。 除此之外,我不认为这是
线程真的很有生产力。

2019 年 10 月 25 日星期五上午 11:59 brunnerh [email protected]写道:

这是一个有意识的决定。 这段代码会很烦人
是一个错误:

var a = [];for (var i =0; i< a.length; i++) {
a[i]+=1; // a[i] 可能未定义
}

嘿,我认得那个语法; 我可能每年写一次这样的循环!

我发现在我实际对数组进行索引的大多数情况下,我
实际上想之后检查未定义。

担心使这种特殊情况更难处理似乎有些夸大其词。
如果您只想迭代可以用于 .. of. 的元素。 如果
由于某种原因您需要元素索引,请使用 forEach 或迭代
条目。 一般来说,你真的需要一个非常罕见的
基于索引的for循环。

如果有更好的理由来解释为什么想要维持现状,我会
喜欢看他们,但不管:这是系统的不一致
似乎有一个标志来修复它会受到许多人的高度赞赏。


您收到此消息是因为您发表了评论。
直接回复本邮件,在GitHub上查看
https://github.com/microsoft/TypeScript/issues/13778?email_source=notifications&email_token=ACAJU3DQ7U6Y3MUUM26J4JDQQM62XA5CNFSM4C6KEKAKYY3PNVWWK3TUL52HS4DFVREXG43VMVBWJKLOJDNPY43VMVBWJKLOJDNPY43VMVBWJKLOJDNPY5707U6Y3MUUM26J4JDQQM62XA5CNFSM4C6KEKAKYY7
或取消订阅
https://github.com/notifications/unsubscribe-auth/ACAJU3EWVM3CUFG25UF5PGDQQM62XANCNFSM4C6KEKAA
.

我觉得,虽然包所有者可能没有动静,但社区的持续反馈仍然很有价值,因为它表明仍然需要更好的工具来解决这个问题。

@brunnerh我同意你的看法,现在我什至不需要使用for循环,除非它是为了性能调优,这在我公司的代码库中发生的时间为 0%,因为大部分时间都在更改地图/filter/reduce to for 循环很少能提高性能,真正的罪魁祸首总是低效的逻辑、网络问题和数据库连接。

我很惊讶还没有人谈论as const

const test = [1, 2, 3] as const;

(test[100]).toFixed(5);
// Tuple type 'readonly [1, 2, 3]' of length '3' has no element at index '100'.

更一般地说,与此问题的初始消息不完全相关,过去几个月我一直在使用以下防御性编程模式,并且效果很好(对我而言)

const xs: Array<number | undefined> = [1,2,3];

// for objects but kind of related as well
Record<string, User | undefined>

interface Something {
  [key: string]: User | undefined
}

尽管它没有简短的符号(如? ),但我觉得它很好。 自己告诉编译器您可能不确定该值是否已定义,恕我直言很好。

@martpie as const如果您可以使用它,那就太好了。

我不想使用Array<T | undefined>至少有两个原因:

  1. 每次使用数组时,您还必须记住要做的另一件事。 此外,您不能再只使用隐式类型,这是很好的。
  2. 它更改了forEachmapfilter的签名,这些签名不会将undefined作为元素参数传递(除非该索引明确设置为这种方式)。 取决于您使用这些功能的程度,这些功能可能很烦人。

我还想补充一点,这会导致现在 eslint/typescript 中出现很多误报:

const a: string[] = [];
const foo = a[1000];
if (foo) { // eslint says this is an unnecessary conditional
  console.log(foo.length);
}

Eslint(正确地)从类型推断这是一个不必要的检查,因为 foo 永远不能为空。 所以现在我不仅必须有意识地记住对我从数组中返回的内容进行空检查,我_还_必须添加一个 eslint 禁用行! 并且像这样访问 for 循环之外的内容实际上是我们进行数组访问的唯一方法,因为(可能就像当今大多数 TS 开发人员一样),我们在循环数组时使用 forEach/map/filter/etc 函数。

我真的认为我们应该有一个新的编译器标志,如果strict为 true,则默认设置为 true,如果人们不喜欢它,他们可以选择退出。 无论如何,我们在最近的版本中添加了新的破坏性严格的编译器检查。

这很可能是我在最近的记忆中看到的 _only_ 运行时 prod 错误的来源,这些错误是类型系统的故障。

考虑到现在可用的可选链运算符的人体工程学,也许是时候重新考虑_不_通过标志使此行为可用的决定了?

如果我从服务器得到一个Thing的数组,并且我需要显示数组中的最后一个Thing ,我经常遇到的事情是数组实际上是空的,并且访问了一个属性在Thing使应用程序崩溃。

例子:

// `things` is `Thing[]`, but is empty, i.e., `[]`
const { things } = data; 

// We are accessing `things[-1]`, which is obviously `undefined`, 
// but TypeScript thinks `latestThing` is a `Thing`
const latestThing = things[things.length - 1];

// TypeError: Cannot read property 'foo' of undefined
return latestThing.foo; 

也许这是我们 API 设计的问题,但感觉 TS 真的应该明白,当您访问数组中的某些内容时,它实际上可能并不存在。

编辑:只是想澄清我的意思是“我们的 API”,如“我正在工作的团队创建的 API”

是的,每个人都非常同意这是一个非常糟糕的设计决定,与 TS 完全不安全的遥远时代是当代的。

作为一种解决方法,我使用了一个小的脏 hack。 我只是添加package.json简单的安装后脚本:

{
...
  "scripts": {
    "postinstall": "sed -i 's/\\[n: number\\]: T;/[n: number]: T | undefined;/g' node_modules/typescript/lib/lib.es5.d.ts",
    ...
  },
...
}

当然,它在 Windows 上不起作用,但这总比没有好。

与此问题相邻的所有其他问题似乎都被重定向到这里,所以我认为这是最好的提问地点:表外的可选索引签名的速记语法是什么? 正如@martpie指出的那样,您必须编写interface Foo { [k: string]: Bar | undefined; } ,这比interface Foo { [k: string]?: Bar; }更不符合人体工程学。

我们可以获得?:运营商支持吗? 如果没有,为什么不呢? 也就是说,人体工程学的好处足以为单个属性定义添加功能——它对索引签名是否“足够有用”?

type Foo = { [_ in string]?: Bar }也有效。 不那么漂亮,但相当简洁。 如果需要,也可以制作自己的Dict类型。 不过,不会反对?:扩展名

这开始感觉像是“Javascript:坏部分”的谈话之一:

ts type Foo1 = { [_ in string]?: Bar } // Yup type Foo2 = { [_: string]?: Bar } // Nope interface Foo3 { k?: Bar } // Yup interface Foo4 { [_: string]?: Bar } // Nope

使用T | undefined作为类型签名真的没有用。 我们需要一种方法来使索引运算符[n]具有T|undefined类型,但是例如在数组上使用map应该给我们T|undefined值,因为在那种情况下我们应该知道它们存在。

@radix对于 Array 上的实际函数,我认为这不会成为问题,因为它们都有自己的类型定义,可以输出正确的类型:例如map : https://github.com/microsoft /TypeScript/blob/master/lib/lib.es5.d.ts#L1331

| undefined构造造成实际经验回归的唯一常见代码用法是在for ... of循环中。 不幸的是(从这个问题的角度来看),这些是相当常见的结构。

@riggs我认为你在谈论让interface Array<T> { }[index: number]: T | undefined ,但@radix可能在谈论似乎是当前推荐的内容,即在Array<T | undefined>中使用你自己的代码。

后者不好有几个原因,其中最重要的是你不控制其他包的类型,但前者也有一些问题,即你可以将undefined分配给数组,并且它即使在已知安全的情况下,也会给出undefined 。 🤷‍♂️

啊,是的,我的误会。 事实上,我只是指使用定义[index: number]: T | undefined 。 我完全同意将类型定义为Array<T | undefined>是一种糟糕的变通方法,它会导致比它解决的问题更多的问题。

是否有一种优雅的方式来覆盖lib.es5.d.ts或者安装后脚本是最好的方法?

@nicu-chiciuc https://www.npmjs.com/package/patch-packagehttps://www.npmjs.com/package/yalc一起完全适合异端的工具箱✌️

是否有一种优雅的方法来覆盖 lib.es5.d.ts 或者安装后脚本是最好的方法?

在我们的项目中,我们有一个global.d.ts文件,我们将其用于 1) 为尚未在 typescript 的默认类型定义中的内置 API 添加类型定义(例如,WebRTC 相关的 API,这些 API 在浏览器之间不断变化且不一致) 和 2) 覆盖一些 typescript 的默认类型定义(例如覆盖Object.entries的类型,以便它返回包含unknown而不是any数组)。

我不确定这种方法是否可以用来覆盖 typescript 的数组类型来解决这个问题,但它可能值得一试。

当声明合并给出你想要的结果时,上面的工作,但为了简化它们相交的接口,但这里限制较少的选项是你想要的。

您可以尝试将您正在使用的所有lib.*.d.ts文件复制粘贴到您的项目中,包括在tsconfig.jsonfilesinclude ,然后编辑它们到你想要的。 由于它们包含/// <reference no-default-lib="true"/> ,因此您不需要任何其他魔法,但我不确定您是否必须删除它们对依赖项的/// <reference lib="..."/> 。 当然,出于所有明显的可维护性原因,这很糟糕。

@butchler我们也在使用这种方法,但似乎无法覆盖索引运算符,尽管我们已经成功覆盖了其他一些数组函数(带有类型保护的filter )。

我有一个覆盖索引运算符的奇怪案例:

function test() {
  const arr: string[] = [];

  const [first] = arr;
  const zero = arr[0];

  const str1: string = first;
  const str2: string = zero;
}

Screenshot 2020-02-05 at 10 39 20 AM

第二个分配错误(应该如此),但第一个没有。
更奇怪的是,当它被解构时悬停在first表明它具有string | undefined的类型,但在分配时悬停在它上面表明它具有string
Screenshot 2020-02-05 at 10 40 25 AM
Screenshot 2020-02-05 at 10 40 32 AM

数组解构是否有不同的类型定义?

寻找解决方案,因为与缺失索引相关的错误一直是我代码中错误的常见来源。

指定像{ [index: string]: string | undefined }这样的类型不是一个解决方案,因为它会打乱像Object.values(x).forEach(...)这样永远不会包含undefined值的迭代器的输入。

当我在执行someObject[someKey]后不检查 undefined 时,我希望看到 TypeScript 生成错误,而不是在执行Object.values(someObject).forEach(...)

@danielnixon这不是解决方案,而是解决方法。 没有什么可以阻止您或其他开发人员错误地(如果您甚至可以这样称呼的话)将语言的内置工具用于相同的目标。 我的意思是,我对这些东西使用 fp-ts,但是这个问题仍然有效并且需要修复。

虽然我可以遵循@RyanCavanaugh反对意见

for (let i = 0; i < arr.length; i++) {
  // TypeScript makes me use ! with my arrays, sad.
  console.log(arr[i]!.name);

我会就此发表一些简短的想法。 从本质上讲,TypeScript 旨在使开发更安全。 记住 TypeScript的第一个目标

1. 静态识别可能出错的结构。

如果我们看一下编译器,就会发现类型推断背后有一些魔法。 在特定情况下,真正的借口是:“我们无法推断出此构造的正确类型”,这完全没问题。 随着时间的推移,我们有越来越少的情况,其中 compile 无法推断类型。 换句话说,对我来说,获得更复杂的类型推断只是时间问题(并花时间在这上面)。

从技术角度来看,构造如下:

const x = ['a', 'b', 'c']
console.log(x[3]) // type: string, reality: undefined

打破了 TypeScript 的第一个目标。 但是如果 TypeScript 知道更精确的类型,则不会发生这种情况:

const x = ['a', 'b', 'c'] as const
console.log(x[3]) // compile error: Tuple type 'readonly ["a", "b", "c"]' of length '3' has no element at index '3'.ts(2493)

从实用的角度来看,这是一种权衡yet 。 这个问题和多个已解决的问题表明社区对这个变化有很高的需求:ATM 238 个赞成票 vs. 2 个反对票。 当然,遗憾的是上面的 for 循环没有推断出“正确”的类型,但是,我很确定大多数支持者可以接受!和新的?作为注意的标志并在安全的情况下强制它。 但另一方面,在“危险”访问中获得正确的类型。

因为我同意对于大型代码库来说这是一个非常敏感的更改,所以我投票支持配置属性,就像这里提出的那样。 至少是为了从社区获得反馈。 如果不可能,那么,TS 4.0 应该会得到它。

这不是建议的解决方案,而只是一个实验:我尝试在使用 TypeScript 的现有项目中修改lib.es5.d.ts node_modules中的lib.es5.d.ts只是为了看看如果我们_did_有一个编译器会是什么样子为此的选项。 我修改了ArrayReadonlyArray

interface ReadonlyArray<T> {
  ...
  [n: number]: T | undefined; // was just T
}

interface Array<T> {
  ...
  [n: number]: T | undefined; // was just T
}

因为这是一个非常大的项目,这导致了数百个类型错误。 我只浏览了其中的几个,以了解我会遇到什么样的问题,以及如果有编译器选项,他们将很难解决。 以下是我遇到的一些问题:

  1. 这不仅在我们的代码库中导致了类型错误,而且在我们的一个依赖项中导致了类型错误:io-ts。 由于 io-ts 允许您创建在您自己的代码中使用的类型,我认为不可能将此选项仅应用于您自己的代码库而不将其应用于 io-ts 的类型。 这意味着 io-ts 和可能的其他一些库仍然需要更新才能使用此选项,即使它只是作为编译器选项引入。 起初我认为将其作为编译器选项会减少争议,但如果选择使用该选项的人开始向一堆不同的库作者抱怨不兼容,那么它可能比仅仅作为 TS 4.0 更具争议性打破变化。

  2. 有时我不得不添加一些额外的类型保护来消除 undefined 的可能性。 这不一定是件坏事,也是本提案的重点,但我只是为了完整性而提及它。

  3. 我在for (let i = 0; i < array.length; i++)循环内有一个类型错误,该循环在任意Array<T>循环。 我不能只添加一个类型保护来检查undefined ,因为T本身可能包含undefined 。 我能想到的唯一解决方案是 A) 使用类型断言或@ts-ignore来消除类型错误或 B) 改用 for-of 循​​环。 就我个人而言,我不认为这太糟糕(可能很少有使用 for-of 循​​环迭代数组的情况并不好),但这可能会引起争议。

  4. 在许多情况下,现有代码已经对.length的值进行了断言,然后访问数组中的元素。 尽管进行了.length检查,但这些现在会导致类型错误,因此我不得不将代码更改为不依赖于.length检查,或者只是添加一个冗余的!== undefined检查。 如果 TypeScript 能够以某种方式允许使用.length检查来避免需要!== undefined检查,那将是非常好的。 不过,我认为实际实施这并非易事。

  5. 一些代码使用A[number]来获取泛型数组类型的元素的类型。 然而,这现在返回T | undefined而不是T ,导致其他地方的类型错误。 我做了一个助手来解决这个问题:

    type ArrayValueType<A extends { [n: number]: unknown }> = (
      A extends Array<infer T> ? T :
      A extends ReadonlyArray<infer T> ? T :
      A[number] // Fall back to old way of getting array element type
    );
    

    但即使有了这个助手来缓解过渡,这仍然是一个很大的突破性变化。 也许 TypeScript 可以为A[number]提供某种特殊情况来避免这个问题,但如果可能的话,最好避免这种奇怪的特殊情况。

我只经历了数百个类型错误中的一小部分,所以这可能是一个非常不完整的列表。

对于有兴趣解决此问题的任何其他人,尝试对一些现有项目执行相同的操作并分享您遇到的其他问题可能会很有用。 希望具体的例子可以为任何真正尝试实现这一点的人提供一些指导。

@butchler我也遇到过其中一些问题,我开始将something[i]视为something(i) ,也就是说,我不能只使用if (Meteor.user() && Meteor.user()._id) {...} ,所以我不不希望使用这种方法进行数组索引; 我必须首先从数组中获取我想要检查的值。 我想说的是,依靠 TS 来理解检查长度属性并基于此做出一些断言可能会给类型系统带来太多压力。 让我推迟转换的一件事(除了一些其他库也有错误的事实,正如你所说的)是数组解构尚未正常工作,解构的值不会是T | undefined但是T (见我的其他评论)。
除此之外,我认为覆盖lib.es5.d.ts是一个很好的方法。 即使事情将来会发生变化,恢复更改不会增加额外的错误,但会确保已经涵盖了一些边缘情况。
我们已经开始使用patch-package来更改react一些类型定义,这种方法似乎工作正常。

我也遇到过其中一些问题,我开始将 something[i] 视为 something(i),也就是说,我不能只使用 if (Meteor.user() && Meteor.user()._id) { ...},所以我不希望有这种数组索引方法; 我必须首先从数组中获取我想要检查的值。

是的,当我尝试我的实验时,我也最终使用了这种方法。 例如,之前编写的代码如下:

if (array[i]) {
  array[i].doSomething(); // causes a type error with our modified Array types
}

必须改为:

const arrayValue = array[i]
if (arrayValue) {
  arrayValue.doSomething();
}

我想说的是,依靠 TS 来理解检查长度属性并基于此做出一些断言可能会给类型系统带来太多压力。

可能是真的。 它实际上可能更容易编写一个codemod自动重新编写代码,取决于对断言.length使用一些其他的办法,不是让打字稿足够聪明来推断更多基于有关断言的类型.length

即使事情将来会发生变化,恢复更改不会增加额外的错误,但会确保已经涵盖了一些边缘情况。

这是个好的观点。 虽然在索引类型中包含undefined是一个巨大的突破性变化,但在另一个方向上并不是一个突破性的变化。 能够处理T | undefined也应该能够处理T而无需任何更改。 这意味着,例如,如果存在编译器标志并且仍被未启用该编译器标志的项目使用,则可以更新库以处理T | undefined情况。

忽略数组,暂时关注Record<string, T> ,我个人的愿望是写入只允许T而读取可能是T|undefined

declare const obj : Record<string, T>;
declare const t : T;
obj["k"] = t; //ok
obj["k"] = undefined; //error, undefined not assignable to T

//T|undefined inferred,
//since we don't know if "k2" is an "ownProperty" of obj
const v = obj["k2"];

上述符合人体工程学的唯一方法是某种依赖类型和依赖类型保护。 由于我们没有这些,添加此行为会导致各种问题。

//Shouldn't just be string[]
//Should also be something like (keyof valueof obj)[],
//A dependent type
const keys = Object.keys(obj);

让我们回到阵中,问题在于,对于数组索引签名具有的意图是相同的Record<number, T>

所以,你需要完全不同的依赖类型保护,比如,

for (let i=0; i<arr.length; ++i) {
  //i is not just number
  //i should also be something like keyof valueof arr 
}

因此,数组的索引签名并不是真正的Record<number, T> 。 它更像是Record<(int & (0 <= i < this["length"]), T> (一个范围和数字支持的整数类型)


因此,虽然原始帖子只是谈论数组,但标题似乎暗示了“只是” string或“只是” number索引签名。 这是两个完全不同的讨论。

TL;DR 原始帖子和标题讨论了不同的事情,实现其中一个看起来严重依赖(ha)依赖类型。

鉴于forEachmapfilter等函数的存在(恕我直言更可取),我不希望 TS 团队将他们的推理引擎复杂化到支持使用常规 for 循环遍历数组。 我有一种感觉,试图实现这一目标有太多意想不到的复杂性。 例如,由于i不是常量,如果有人在循环内更改i的值会发生什么? 当然,这是一个边缘情况,但这是他们需要以(希望)直观的方式处理的事情。

然而,修复索引签名应该相对简单(ish),如上面的评论所示。

4. 在很多情况下,现有代码已经对.length的值进行了断言,然后访问数组中的元素。 尽管有.length检查,但这些现在会导致类型错误,所以我不得不更改代码以不依赖于.length检查,或者只是添加一个冗余的!== undefined检查。 如果 TypeScript 能够以某种方式允许使用.length检查来避免需要!== undefined检查,那将会非常好。 不过,我认为实际实施这并非易事。

@布彻勒
出于好奇,其中有多少是使用不断变化的变量访问的,而不是使用固定数字访问的? 因为,在后一种情况下,您大概将数组用作元组,为此 TS 会跟踪数组长度。 如果您经常使用数组作为元组,我很好奇将它们重新键入为实际的元组对错误量的影响。

如果您经常使用数组作为元组,我很好奇将它们重新键入为实际的元组对错误量的影响。

我确实遇到过一种情况,我通过向数组文字添加as const来将变量的类型转换为元组来修复类型错误。 不过,我完全不知道这在我们的代码库中有多常见,因为我只查看了数百个错误中的 10 个。

我今天也遇到了这个问题。
我们通过计算索引从数组访问。 该指数可能超出范围。
所以我们的代码中有这样的东西:

const noNext = !items[currentIndex + 1];

这导致noNext被定义为false 。 这是错误的。 这可能是真的。
我也不想将items定义为Array<Item | undefined>因为这给出了错误的期望。
如果有索引,它永远不应该是undefined 。 但是如果你使用了错误的索引,它 _is_ undefined
当然,上述问题可能可以通过使用.length检查或将noNext明确定义为boolean
但最终,自从我开始使用 TypeScript 以来,这一直困扰着我,我一直不明白为什么默认情况下不包含| undefined

由于我希望大多数人使用 typescript-eslint,是否有任何规则可以强制在索引后必须检查值才能使用它们? 在实现 TS 的支持之前,这可能会有所帮助。

万一有人感兴趣。 在之前的评论中,我提到在lib.es5.d.ts中修补Array索引定义的方法在数组解构方面有一些奇怪的行为,因为它似乎不受更改的影响。 看来,这取决于target的选项tsconfig.json它正常工作时,它是es5 (https://github.com/microsoft/TypeScript/issues/37045 )。

对于我们的项目,这不是问题,因为我们使用noEmit选项,并且转译由meteor 。 但是对于可能存在问题的项目,一个可行的解决方案是safe-array-destructuring (https://github.com/typescript-eslint/typescript-eslint/pull/1645)。
它仍然是一个草案,当整个问题将在 TypeScript 编译器中修复时,它可能没有价值,但如果您认为它可能有用,请随时解决typescript-eslint规则 PR中的任何问题/改进

可悲的是,在您的 PR 中使用 eslint 的修复仅支持元组而不是数组。 我对此的主要问题和主要影响是数组解构。

const example = (args: string[]) => {
  const [userID, nickname] = args
}

我认为整个问题都站在我不同意的一边。 我不认为应该在 forEach、map 等内部进行强制检查......我也尽可能避免使用 for,所以避免这种情况的理由对我来说意义不大。 尽管如此,我仍然认为这对于支持数组解构至关重要。

好吧,它只是对任何数组解构都会出错,所以你不得不使用索引来完成它。

不过这不是很糟糕吗? 数组解构是一个了不起的特性。 问题是打字不准确。 禁用该功能并不是一个好的解决方案。 在我看来,使用索引会使代码更丑(更难阅读和理解),而且更容易出错。

它更难看,但它迫使开发人员检查元素是否已定义。 我试图提出更优雅的解决方案,但似乎这是缺点最少的解决方案。 至少对于我们的用例。

@caseyhoward如本期前面所述,这会导致各种 Array.prototype 函数出现不希望的行为:

x.forEach( (i: string) => { ... } )  // Error because i has type string | undefined

这不需要在 array.prototype 函数中修复。 这是数组解构的一个大问题!

今天又碰到这个了。 我仍然认为正确的妥协是这个,我很想得到一些反馈。

这个问题的另一个用例——我在使用 typescript-eslint 并启用no-unnecessary-condition时遇到了这个问题。 过去,当通过索引访问数组以对该索引处的元素进行一些操作时,我们使用可选链来确保该索引已定义(以防索引超出范围),如array[i]?.doSomething() 。 然而,对于本期中概述的问题, no-unnecessary-condition将可选链标记为不必要的(因为类型根据打字稿不可为空)并且自动修复删除了可选链,从而导致在访问数组时出现运行时错误索引i实际上未定义的。

如果没有这个功能,我的应用程序在处理多维数组时变得非常有问题,我必须始终提醒自己使用xs[i]?.[j]而不是xs[i][j]访问元素,我还必须明确地强制转换访问元素如const element = xs[i]?.[j] as Element | undefined以确保类型安全。

遇到这个是我在 Typescript 中的第一个大型 wtf。 否则我发现该语言非常可靠,这让我失望,并且将“as T | undefined”添加到数组访问中非常麻烦。 希望看到其中一项提案得到实施。

是的,这里也一样。 它迫使我们推出我们自己的类型( | undefined )以具有类型安全性。 主要用于对象访问(可能是一个单独的开放问题),但相同的逻辑适用于访问数组索引。

不确定您的意思,您可以链接问题或显示示例吗?

2020 年 4 月 29 日星期三上午 2:41 Kirill Groshkov通知@ github.com
写道:

是的,这里也一样。 它迫使我们推出自己的具有 ( |
未定义)具有类型安全性。 主要用于对象访问(可能是一个
单独打开的问题),但相同的逻辑适用于访问数组索引。


您收到此消息是因为您发表了评论。
直接回复本邮件,在GitHub上查看
https://github.com/microsoft/TypeScript/issues/13778#issuecomment-621096030
或取消订阅
https://github.com/notifications/unsubscribe-auth/AAAGFJJT37N54I7EH2QLBYDRO7Y4NANCNFSM4C6KEKAA
.

@alangpierce

一个看起来很有希望的想法:如果您可以在定义索引签名时使用?:来指示您希望在正常使用下有时会丢失值怎么办? 它就像上面提到的| undefined ,但没有尴尬的缺点。 它需要禁止显式undefined值,我想这与通常的?:

这是一个有趣的想法,指向正确的方向。 但是,我会颠倒逻辑:默认情况下应该是索引签名包括| undefined 。 如果您想告诉编译器未定义的情况永远不会出现在您的代码中(因此您永远不会访问无效密钥),您可以将!:到签名中,如下所示:

type AlwaysDefined = {[key: string]!: string};
const t: AlwaysDefined = {};

t['foo'] // Has type string

没有!:的默认情况看起来就像我们知道的那样,但会更安全:

type MaybeUndefined = {[key: string]: string};
const t: MaybeUndefined = {};

t['foo'] // Has type string | undefined

这样你在默认情况下就有了安全性,同样你不必明确包含undefined ,这将允许意外写入undefined

抱歉,如果已经有人建议了,我无法阅读此线程中的所有评论。

总的来说,我真的认为我们现在拥有的默认行为并不是用户在使用 TypeScript 时所期望的。 安全总比抱歉好,尤其是当编译器可以像这个错误一样轻松捕获错误时。

我无法让 index.d.ts 与我的项目一起工作,也不想处理它。

我创建了这个强制类型保护的小技巧:

 const firstNode = +!undefined && nodes[0];

尽管类型最终为: 0 | T ,但它仍然可以完成工作。

const firstNode = nodes?.[0]行不通?

@ricklove 有什么理由让您更喜欢+!undefined不是简单的1吗?

const firstNode = nodes?.[0]行不通?

不,因为打字稿(在我看来是错误的)不会将其视为可选的(请参阅 #35139)。

根据Flow 的文档,索引签名的行为与当前的 TypeScript 相同:

当对象类型具有索引器属性时,即使对象在运行时在该槽中没有值,也假定属性访问具有带注释的类型。 程序员有责任确保访问是安全的,就像使用数组一样。

var obj: { [number]: string } = {};
obj[42].length; // No type error, but will throw at runtime

所以在这个问题上,TS 和 Flow 都要求用户考虑未定义的键,而不是编译器帮助他们:(

如果编译器阻止用户制造这些类型的错误,这将是 TS 的一个优势。 阅读此线程中的所有评论,似乎这里的绝大多数人都希望拥有此功能。 TS战队还是100%反对吗?

TS 尽最大努力阻止访问未定义的成员似乎很愚蠢,除了这个。 这是迄今为止我在我们的代码库中修复的最常见的错误。

[从#38470 移动讨论在这里]
接受所有关于为什么这对数组不实用的论点..
我认为这绝对应该为元组解决:

// tuple 
var str = '';
var num = 100
var aa = [str, num] as const

// Awesome
var shouldBeString = aa[0] // string
var shouldBeNumber = aa[1] // number
var shouldError = aa[10000]; // type error

// Not so awesome 
var foo = aa[num] // string | number

为什么不让 foo string | number | undefined

将此赋予元组不应影响大多数用户,它影响的用户需要启用strictNullChecks ,并将其数组声明为常量......
他们显然在寻找更严格的类型安全性。

也可以帮助解决这些问题

var notString1 = aa[Infinity]; // no error, not undefined
var notString2 = aa[NaN]; // no error, not undefined

这最初导致我的运行时错误,因为number变成了NaN ,然后从元组返回了undefined ..
所有这些都是类型安全的

我不确定元组是否真的如此不同。 他们必须适应休息类型(例如[str, ...num[]] ),并且从技术上讲aa[Infinity]是完全有效的 Javascript,所以我可以看到为他们创造一个特殊情况变得很复杂。

你的帖子也让我思考如果我们确实得到一些支持将索引返回视为未定义,它会是什么样子。 如果在您的示例中, aa[num]实际上确实键入了string | number | undefined ,即使我知道索引将保持在边界内,我是否还必须编写for (let i = 0; i < 2; i++) { foo(aa[i]!); } ? 对我来说,当严格的空检查标记我写的东西时,我喜欢能够通过首先输入更好的东西或使用运行时保护来修复它——如果我不得不求助于非空断言,我通常将其视为我的失败。 不过,在这种情况下,我没有找到解决方法,这让我很困扰。

如果tuple[number]总是输入到T | undefined ,你想如何处理你知道索引被正确界定的情况?

@thw0rted我不记得上次在 Typescript( .map.reduce ftw)中使用“经典” for循环是什么时候。 这就是为什么为此提供一个编译器选项会很棒的原因(默认情况下可以关闭)。 许多项目从未遇到过您提供的情况,并且仅使用索引签名进行数组解构等。

(我原来的评论:https://github.com/microsoft/TypeScript/issues/13778#issuecomment-517759210)

老实说,我也很少用 for 循环索引。 我广泛使用Object.entriesArray#map ,但每隔一段时间我需要传递键来索引到一个对象或(可能很少)索引到一个数组或元组。 for循环当然是一个人为的例子,但它提出了一个观点,即任一选项(或不undefined )都有缺点。

但它提出了任何一个选项( undefined或不)都有缺点的观点。

是的,如果 TypeScript 的用户可以选择他们喜欢的缺点,那就太好了 :wink:

老实说,我也很少用 for 循环索引。 我广泛使用Object.entriesArray#map ,但每隔一段时间我需要传递键来索引到一个对象或(可能很少)索引到一个数组或元组。

在您确实需要的情况下,您可以使用Array#entries

for (const [index, foo] of array.entries()) {
    bar(index, foo)
}

我个人并不经常使用Array#forEach ,而且我从不使用Array#map进行迭代(尽管我一直使用它进行映射)。 我更喜欢保持我的代码平坦,并且我很欣赏能够跳出 for...of 循环的能力。

我不确定元组是否真的如此不同。 他们必须适应休息类型(例如[str, ...num[]] ),并且从技术上讲aa[Infinity]是完全有效的 Javascript,所以我可以看到为他们创造一个特殊情况变得很复杂。

你的帖子也让我思考如果我们确实得到一些支持将索引返回视为未定义,它会是什么样子。 如果在您的示例中, aa[num]实际上确实键入了string | number | undefined ,即使我知道索引将保持在边界内,我是否还必须编写for (let i = 0; i < 2; i++) { foo(aa[i]!); } ? 对我来说,当严格的空检查标记我写的东西时,我喜欢能够通过首先输入更好的东西或使用运行时保护来修复它——如果我不得不求助于非空断言,我通常将其视为我的失败。 不过,在这种情况下,我没有找到解决方法,这让我很困扰。

如果tuple[number]总是输入到T | undefined ,你想如何处理你知道索引被正确界定的情况?

我不确定有那么多for(let i..)迭代元组..
如果元组只有一个类型,那么用户可能会使用一个数组,

根据我的经验,元组非常适合混合类型,如果这是一般情况,那么您无论如何都要进行类型检查

var str = '';
var num = 100
var aa = [str, num] as const
for (let i = 0; i < aa.length; i++) {
 aa[i] // needs some type check anyway to determine if 'string' or 'number'
}

还假设这个提议的改变发生了什么阻止人们这样做?

// this sucks
for (let i = 0; i < 2; i++) { 
   foo(aa[i]!); // ! required
}

// this would work
for (let i = 0 as 0 | 1; i < 2; i++) { 
   foo(aa[i]); //  ! not required 
}

当他们解决此问题时,您可以使用keyof typeof aa而不是0 | 1

当然在做i算术时不会有任何类型安全,
但它确实允许人们通过默认的“非严格类型安全但方便”数组来选择它以实现类型安全

我认为拥有???.运算符意味着随机访问Array不再过于麻烦
但是,这可能更常见

  • 迭代(例如,使用.map.forEach ,其中不需要“ T | undefined ”,回调永远不会在越界索引处运行)或
  • 在循环中遍历Array ,其中一些可以静态地确定为安全的( for...of .. 几乎,除了 _sparse 数组_, for...in我认为是安全的)。

而且,如果首先使用<index> in <array>检查变量,则可以将其视为“安全索引”。


_Clarification_:我说的是Array<T>没有索引类型[number]: T | undefined (而是只有[number]: T )的最初原因,那就是使用起来太麻烦了。
我的意思是情况不再如此,因此可以添加undefined

@vp2177问题不在于功能(是的, ?.有效),问题在于编译器没有警告我们,并防止我们踩到脚。

也许 linting 规则可以做到,但仍然如此。

是的, ?.有效

我不敢苟同。 ?.将用于访问索引值的属性,但编译器仍然会说该值已定义,无论是否定义(请参阅#35139)。

除此之外,如果您使用 eslint 并想使用no-unnecessary-condition ,此类访问将被标记为不必要并被删除,因为编译器说它永远不会未定义,这将使?.变得不必要。

@martpie抱歉,我认为您误解了我的评论,并进行了澄清。

我的意思是……拥有一个--strictIndexChecks或类似的东西似乎是个好主意,允许人们选择这种行为。 然后它不会对每个人来说都是一个重大变化,它提供了一个更严格的类型环境。

我确实相信Flow默认以这种方式用于索引并且没有上述问题,但是自从我使用它已经有一段时间了,所以我可能会弄错。

@bradennapier Flow不使用T | undefined或者在用于索引签名Array S(所以相同打字稿,不幸的是)。

我之前曾提出过一种可以让每个人都开心的方法。 希望可以用其他方式重复一遍。

基本上索引签名的默认行为将更改为

  • 从数组或对象读取时包含| undefined
  • 写入时不要包含| undefined (因为我们只需要T对象中的值)

定义将是这样的:

type MaybeUndefined = {[key: string]: string};
const t: MaybeUndefined = {};

const x = t['foo'] // Has type string | undefined
t['foo'] = undefined // ERROR! 
t['foo'] = "test" // Ok

对于不希望undefined包含在类型中的人,他们可以使用编译器选项禁用它,或者使用!运算符仅对某些类型禁用它:

type AlwaysDefined = {[key: string]!: string};
const t: AlwaysDefined = {};

const x = t['foo'] // Has type string
t['foo'] = undefined // ERROR! 
t['foo'] = "test" // Ok

为了不破坏现有代码,最好引入一个新的编译器选项,它会导致包含undefined (如上面的MaybeUndefined类型所示)。 如果未指定该选项,则所有索引签名类型的行为就像在声明中使用了!运算符一样。

另一个想法出现:人们可能希望在代码中的某些地方使用相同的类型,并具有两种不同的行为(包括或不包括undefined )。 可以定义新的类型映射:

type MakeDefined<T> = {[K in keyof T]!: T[K]}

这会是 TypeScript 的一个很好的扩展吗?

我喜欢这个想法,但我怀疑我们必须先获得https://github.com/microsoft/TypeScript/issues/2521——不要误会我的意思,我仍然相信我们绝对应该,但我'我没有屏住呼吸。

@bradennapier Flow不使用T | undefined或者在用于索引签名Array S(所以相同打字稿,不幸的是)。

嗯,对象索引呢? 我从来没有遇到过对数组的需求,但相当肯定它会让你在使用对象时进行检查。

@bradennapier对象索引? 如, o[4]其中o是类型object ? TypeScript 不允许你这样做......

类型“4”的表达式不能用于索引类型“{}”
类型“{}”.ts(7053) 上不存在属性“4”

@RDGthree在所有事情上都不是微软的白人骑士,但您显然在第一个回复中错过了 mhegazy 的这一点:

除了strictNullChecks,我们没有改变类型系统行为的标志。 标志通常启用/禁用错误报告。

稍后关于 RyanCavanaugh 有:

我们仍然非常怀疑任何人是否会在实践中从这个标志中获得任何好处。 地图和类似地图的东西已经可以选择加入 | 在他们的定义站点上未定义,并且在数组访问上强制执行类似 EULA 的行为似乎并不成功。 我们可能需要大幅改进 CFA 和类型保护以使其变得可口。

所以基本上 - 他们完全意识到需要一面旗帜的事实,到目前为止,他们还没有相信为他们所做的工作值得,因为人们认为人们会从中获得相当可疑的好处。 老实说,我并不感到惊讶,因为每个人都带着几乎完全相同的建议和例子来到这里,这些建议和例子已经得到了解决。 理想情况下,您应该只对数组使用for of或方法,并使用Map而不是对象(或者效率较低的 'T | undefined` 对象值)。

我在这里是因为我维护了一个流行的 DT 包,人们一直要求将|undefined添加到诸如 http 标头映射之类的东西中,这是完全合理的,除了它会破坏几乎所有现有用法的一点,以及它使Object.entries()等其他安全用法变得更糟糕的事实。 除了抱怨你的意见如何比 Typescript 的创造者好,你在这里的实际贡献是什么?

西蒙,也许我误读了您的评论,但这听起来像是支持原始建议的论据。 “最后一英里”缺失的功能是有时将属性视为| undefined ,具体取决于上下文,但在其他情况下则不然。 这就是为什么我对#2521 上行线程进行了类比。

在理想情况下,我可以声明一个数组或对象,这样,给定

ts const arr: Array<T>; const n: number; const obj: {[k: K]: V}; const k: K;

我可以以某种方式结束

  • arr[n]类型为T | undefined
  • arr[n] = undefined是一个错误
  • 我可以访问arr上的某种迭代,它为我提供了键入为T ,而不是T | undefined
  • obj[k]类型为V | undefined
  • obj[k] = undefined是一个错误
  • Object.entries()应该给我[K, V]元组,并且没有什么是未定义的

在我看来,定义这个问题的是问题的不对称性质。

我会说,对于使用 for 循环的开发人员来说这是一个问题的说法没有考虑到使用数组解构的开发人员。 由于此问题,它在 Typescript 中已损坏,也不会修复。 它提供了不准确的打字。 与仍在使用 for 循环的那些相比,受到影响的 IMO 数组解构问题要大得多。

const example = (args: string[]) => {
  const [userID, duration, ...reason] = args
  // userID and duration is AUTOMATICALLy inferred to be a string here. 
 // However, if for whatever reason args is an empty array 
// userID is actually `undefined` and NOT a `string`. 

// This is valid but it should not be because userID could be undefined
userID.toUpperCase()
}

我不想离问题的原点太远,但是您确实应该使用元组类型作为函数参数来声明该示例方法。 如果函数被声明为([userId, duration, ...reason]: [string, number, ...string[]]) => {}那么你就不必担心这一点。

我的意思是确定我可以投射它,但这并不是真正的解决方案。 同样,这里的任何人都可以简单地转换 for 循环数组索引以及使整个问题静音。

for (let i = 0; i < array.length; i++) {
  const value = array[i] as string | undefined
}

问题是 Typescript 不会针对这种行为发出警告。 作为开发人员,我们必须记住将这些手动转换为 undefined,这对于工具来说是一种不好的行为,会给开发人员带来更多的工作。 公平地说,演员实际上必须看起来像这样:

const example = (args: [string | undefined, string | undefined, ...string[] | ...undefined[]]) => {

}

这一点都不好。 但是就像我上面说的,主要问题甚至不需要像这样输入,TS 根本没有为此发出警告。 这导致开发人员忘记了推送代码,cool CI 在 tsc 合并和部署上没有发现任何问题。 TS 给人一种对代码的信心,而提供的不准确的类型给人一种错误的信心。 => 运行时错误!

这不是“强制转换”,而是正确键入您的形式参数。 我的观点是,如果您正确键入您的方法,那么任何使用旧签名( args: string[] )不安全地调用它的人现在都将在编译时收到错误,因为他们应该这样做,而当他们不能保证时传递正确数量的参数。 如果你想让人们在没有传递你需要的所有参数的情况下逃脱,并在运行时自己检查它们,实际上很容易写成(args: [string?, number?, ...string[]]) 。 这已经很好用了,并且与这个问题并不真正相关。

那只会将问题从该函数转移到另一个调用该函数的函数。 由于您仍然需要将用户提供的字符串解析为单独的值,并且如果您使用数组解构在调用此函数的 commandHandler 中解析它,您最终会遇到同样的问题。

无法避免为数组解构提供不准确的类型。

我不想离问题的原点太远

注意:虽然我同意你的看法,但它们应该被视为单独的问题,只要有人打开一个显示数组解构问题的问题,它就会被关闭并且用户在这里转发。 #38259 #36635 #38287 等等... 所以我不认为我们偏离了这个问题的主题。

我对这个问题的一些不满可以通过一些简单的语法来解决,将| undefined应用于否则将从对象访问中推断出的类型。 例如:

const complexObject: { [key: string]: ComplexType } = { ... };
const maybeKey = 'two';

// From:
const maybeValue = complexObject[maybeKey] as
  | (Some<Complex<Type>> & Pick<WithPlentyOf, 'Utility'> & Types)
  | undefined;

// To:
const maybeValue2 = complexObject[maybeKey] as ?;

所以更严格的类型检查仍然是可选的。 下面的工作,但需要重复(并迅速增加了类似数量的视觉噪音):

const maybeValue2 = complexObject[maybeKey] as
  | typeof complexObject[typeof maybeKey]
  | undefined;

我想这种“类型断言可能”的语法( as ? )也可以更容易地实现require-safe-index-signature-access eslint 规则,这对新用户来说并不是非常混乱。 (例如“您可以通过在末尾添加as ?来修复 lint 错误。”)该规则甚至可以包含一个安全的自动修复程序,因为它只会导致编译中断,而不会导致运行时问题。

function eh<T>(v: T): T | undefined {
    return v;
}

const arr = [0, 1, 2, 3, 4, 5];
const thing1 = arr[1]; // number
const thing2 = eh(arr[1]); // number | undefined

我认为除了这个令人惊讶的 TS 功能之外,没有任何应用程序可以使用它,呵呵

function eh<T>(v: T): T | undefined {
    return v;
}

const arr = [0, 1, 2, 3, 4, 5];
const thing1 = arr[1]; // number
const thing2 = eh(arr[1]); // number | undefined

感谢您的回复。 虽然像这样的函数可以工作(并且几乎肯定会被大多数 JS 引擎优化掉),我宁愿接受提到的额外语法噪音( as ComplexType | undefined )也不愿让编译的 JS 文件保留这个方法“包装器” “任何使用它的地方。

另一个语法选项(与上面的as ?相比用例更少)可以允许在类型断言中使用infer关键字作为其他推断类型的占位符,例如:

const maybeValue = complexObject[maybeKey] as infer | undefined;

目前infer声明中只允许extends条件类型(如条款ts(1338) ),所以给出infer中的上下文关键字含义as [...]类型断言不会干扰。

通过 eslint 规则强制使用infer | undefined仍然是合理的,同时对于其他情况也足够通用,例如可以使用实用程序类型对结果类型进行细化:

// Strange example, maybe from an unusual compiler originally written in JS:
if(someRegularExpression.test(maybeKey)) {
  /**
  * If we are inside this code block and this `maybeKey` exists on `partialObject`, 
  * we know its `regexSuccess` must also be defined, though this knowledge 
  * cannot be encoded using TypeScript's type system.
  */
  const maybeValue = partialObject[maybeKey] as (Required<Pick<infer, 'regexSuccess'>> & infer) | undefined;
  // ...
}
// Here we don't know if `regexSuccess` is defined on `maybeValue2`:
const maybevalue2 = partialObject[maybeKey] as infer | undefined;
function eh<T>(v: T): T | undefined {
    return v;
}

const arr = [0, 1, 2, 3, 4, 5];
const thing1 = arr[1]; // number
const thing2 = eh(arr[1]); // number | undefined

如果我们必须始终记住使用不同的、更安全的模式,那将毫无用处。 关键是让 TypeScript 支持我们,无论我们是在美好的一天,还是对代码库感到疲倦或新手。

我们当前的行为是类型表达式Array<string>[number]被解释为“当你用数字索引数组时出现的类型”。 你可以写

const t: Array<string>[number] = "hello";

作为编写const t: string的迂回方式。

如果这个标志存在,会发生什么?

如果Array<string>[number]string | undefined ,那么您在写入时存在健全性问题:

function write<T extends Array<unknown>>(arr: T, v: T[number]) {
    arr[0] = v;
}
const arr = ["a", "b", "c"];
// Would be OK
write(arr, undefined);

如果Array<string>[number]string ,那么您遇到的问题与阅读中描述的问题相同:

function read<T extends Array<unknown>>(arr: T): T[number] {
    return arr[14];
}
const arr = ["a", "b", "c"];
// Would be OK
const k: string = read(arr);

为“索引读取类型”和“索引写入类型”添加类型语法似乎过于复杂。 想法?

我应该补充一点,更改Array<string>[number]的含义确实有问题,因为这意味着该标志会更改声明文件的解释。 这令人伤脑筋,因为这似乎是很多人都会启用的标志,但可能会导致在从绝对类型发布的内容中发生/不发生错误,因此我们可能必须确保一切都构建两个方向都干净利落。 我们可以通过这种行为添加的标志数量显然非常少(我们不能 CI 测试每种类型包的 64 种不同配置)所以我认为我们必须将Array<T>[number]保留为T只是为了避免这个问题。

@RyanCavanaugh在您使用write(arr, undefined)示例中,将接受对write的调用,但是赋值arr[0] = v;不再编译吗?

如果上面的write函数来自一个库,并且是在没有标志的情况下编译的 JS,那么肯定它是不健全的,但是,这已经是一个存在的问题,因为库可以使用他们选择的任何标志进行编译。 如果一个库是在没有严格空检查的情况下编译的,那么其中的一个函数可能会返回string ,而它实际上应该返回string|undefined 。 但是现在大多数图书馆似乎都实现了空检查,否则人们会抱怨。 同样,一旦这个新标志存在,希望图书馆开始实施它,最终大多数图书馆都会设置它。

此外,虽然我无法在脑海中找到任何具体的例子,但我当然必须设置skipLibCheck才能编译我的应用程序。 每当创建新的 TS 存储库时,它都是我们样板中的标准 tsconfig 选项,因为我们遇到了很多问题。

就个人而言(也可能是自私的),如果它使 _my_ 代码通常更安全,我可以在边缘情况下受到打击。 我仍然相信我会比我遇到其他边缘情况更频繁地通过索引访问数组而遇到空指针,尽管我可以被说服否则。

如果数组[数字] 是字符串 | 未定义,那么您在写入时存在健全性问题:

在我看来, Array<string>[number]应该总是string|undefined 。 这是现实,如果我用任何数字索引到一个数组,我将得到数组的项目类型或未定义。 除非您像使用元组一样对数组长度进行编码,否则您真的不能再具体了。 您的编写示例不会键入检查,因为您不能将string|undefined分配给数组索引。

为“索引读取类型”和“索引写入类型”添加类型语法似乎过于复杂。 想法?

这似乎正是应该的,因为它们是两种不同的东西。 任何数组都不会定义索引,因此您总是有可能未定义。 Array<string>[number]的类型是string|undefined 。 为了指定你想要的TArray<T> ,实用型,可以使用(命名不是很大): ArrayItemType<Array<string>> = string 。 这对Record类型没有帮助,它可能需要类似RecordValue<Record<string, number>, string> = string

我同意这里没有很好的解决方案,但我很确定我更喜欢索引读取的健全性。

我对数组的这种需求没有特别强烈的感觉,因为许多其他语言(包括像 Rust 这样的“安全”语言)将边界检查的责任留给了用户,所以我和许多开发人员已经习惯了这样做. 此外,在这些情况下,语法往往使其变得非常明显,因为它们_总是_使用括号表示法,如foo[i]

也就是说,我 _do_ 强烈建议将其添加到字符串索引对象中,因为很难判断foo.bar的签名是否正确(由于字段bar被显式定义),或者可能是undefined (因为它是索引签名的一部分)。 如果这个案例(我认为它的价值很高)得到解决,那么数组案例可能变得微不足道,也可能值得做。


为“索引读取类型”和“索引写入类型”添加类型语法似乎过于复杂。 想法?

javascript 中的 getter 和 setter 语法可以扩展到类型定义。 例如,TypeScript 完全理解以下语法:

const foo = {
  _bar: "",
  get bar(): string {
    return this._bar;
  },
  set bar(value: string) {
    this._bar = value;
  }
}

但是,TS 目前将这些“合并”为单一类型,因为它不会分别跟踪字段的“get”和“set”类型。 这种方法适用于大多数常见情况,但有其自身的缺点,例如要求 getter 和 setter 共享相同的类型,并且错误地将 getter 类型分配给仅定义了 setter 的字段。

如果 TS 为所有字段分别跟踪“get”和“set”(这可能会带来一些实际的性能成本),它将解决这些问题,并提供一种机制来描述此问题中描述的功能:

type foo = {
  [key: string]: string
}

基本上会成为以下的简写:

type foo = {
  get [key: string](): string | undefined;
  set [key: string](string): string;
}

我应该补充一点,改变 Array 的含义[number] 确实有问题,因为它暗示此标志会更改声明文件的解释。

如果生成的声明文件总是使用完整的 getter+setter 语法,这将_仅_成为手写文件的问题。 将当前规则应用于剩余的速记语法 _仅在定义文件中_ 可能是这里的解决方案。 它肯定会解决兼容性问题,但也会增加阅读.ts文件与.d.ts文件的心理成本。


在旁边:

当然,这并没有解决_all_getter/setter 的微妙警告:

foo.bar = "hello"

// TS assumes that bar is now a string, which technically isn't guaranteed when
// custom setters and getters are used.
const result: string = foo.bar

这是一个进一步的边缘情况,如果它在 TS 目标的范围内,可能值得考虑……但无论哪种方式,它都可能单独解决,因为我在这里的建议与 TypeScript 的当前行为一致。

我真的觉得我只需要在每几页评论中弹出并再次删除指向#2521 的链接。

为“索引读取类型”和“索引写入类型”添加类型语法似乎过于复杂。 想法?

不! 至少(点击链接...)这里还有 116 个人同意我的看法。 我很高兴至少可以选择以不同的方式输入读取和写入。 这只是该功能的另一个很好的用例。

我会_很高兴_至少可以选择以不同的方式输入读取和写入。 这只是该功能的_又一个_很好的用例。

我相信目的是质疑是否应该有一种方法来引用读取和写入类型,例如是否应该有像Array<string>[number]=这样的东西来引用写入类型?

如果发生以下两件事,我会对 #2521 和此问题的解决方案感到满意:

  • 首先,按照 #2521 不同地键入 getter 和 setter
  • 然后,创建本期讨论的标志,使得Array<string>[number]的“写入类型”为string而“读取类型”为string | undefined

我(错误地?)认为前者将为后者奠定基础,请参阅https://github.com/microsoft/TypeScript/issues/13778#issuecomment -630770947。 我对自己定义“写入类型”的显式语法没有任何特别的需求。

@RyanCavanaugh快速元问题:为了更清楚地辩论和跟踪其优势/劣势/成本(特别是因为性能成本非常重要),将我上面相当具体的建议作为一个单独的问题是否会更有用? 或者这只会增加问题的噪音?

@瑞安卡瓦诺

const t: Array<string>[number] = "hello";

作为编写const t: string的迂回方式。

我想这与元组的推理相同吗?

const t : [string][number] = 'hello' // const t: string

但是元组知道边界并正确返回undefined以获取更具体的数字类型:

const t0: [string][0] = 'hello' // ok
const t1: [string][1] = 'hello' // Type '"hello"' is not assignable to type 'undefined'.

// therefore this is already true!
const t2: [string][0|1] // string | undefined 

为什么键入number会有所不同?

看起来你有这样做的机制: https :

这将满足我的需求,而无需添加额外的标志。

如果它对任何人有帮助,这里有一个 ESLint 插件,它标记不安全的数组访问和数组/对象解构。 它不标记元组和(非记录)对象等安全用法。

https://github.com/danielnixon/eslint-plugin-total-functions

干杯,但我希望很明显这仍然需要在语言级别添加,因为它纯粹是在类型检查领域内,并且一些项目没有 linter 或正确的规则。

@瑞安卡瓦诺

我们还测试了这个功能在实践中的样子,我可以告诉你它并不漂亮

只是好奇,你能告诉我们不漂亮的部分是什么吗? 如果你能分享更多关于这方面的信息,也许我们可以解决它。

这不应该是一个选项,它应该是默认的

也就是说,如果 TypeScript 的类型系统要在编译时描述 JavaScript 值在运行时可以采用的类型:

  • 索引Array当然可以在 JavaScript 中返回undefined (索引越界或稀疏数组),因此正确的读取签名将是T | undefined
  • “导致” Array的索引为undefined也可以通过delete关键字。

由于 TypeScript 4 阻止

const a = [1, 2]
delete a[1]

也有一个很好的例子来防止a[1] = undefined
这表明Array<T>[number]对于读取和写入确实应该不同。

它可能是“复杂的”,但它可以更准确地对 JavaScript 中的运行时可能性进行建模;
TypeScript 是为什么设计的,对吧?

讨论让| undefined返回默认行为的想法是没有意义的——他们永远不会发布一个 Typescript 版本,因为编译器更新,它只会爆炸数百万行遗留代码。 在我所关注的任何项目中,这样的更改将是最具破坏性的更改。

不过,我确实同意帖子的其余部分。

讨论让| undefined返回默认行为的想法是没有意义的——他们_永远_不会发布一个 Typescript 版本,由于编译器更新,它只会爆炸数百万行遗留代码。 在我所关注的任何项目中,这样的更改将是最具破坏性的更改。

不过,我确实同意帖子的其余部分。

如果它是配置选项,则不会。

如果它是配置选项,则不会。

这是公平的,但至少我希望有一个很长的“导入”期,默认情况下它是关闭的,让人们有很多时间(6 个月?一年?更多?)在遗留代码变为开启之前转换它-默认。 例如,在 v4.0 中添加标志,在 v5.0 中将其设置为默认开启,诸如此类。

但在这一点上,这是遥远的未来,因为我们甚至不认为先决条件功能(setter/getter 的不同类型)值得做。 所以我支持这样的说法,即长时间讨论默认行为是没有意义的。

讨论让| undefined返回默认行为的想法是没有意义的——他们_永远_不会发布一个 Typescript 版本,由于编译器更新,它只会爆炸数百万行遗留代码。 在我所关注的任何项目中,这样的更改将是最具破坏性的更改。

不过,我确实同意帖子的其余部分。

对于你的观点@thw0rted我在这里提出了一个妥协: https :

它允许这个功能,在一个很晚才引入的功能中,仍然没有被广泛使用,而且它似乎“几乎”已经存在,只需要进行微小的更改。

为了"strict"一致性, Array<T>应该被视为"a list of type T that is infinitly long with no gaps"任何时候都不需要,你应该使用元组。

是的,这有缺点,但我再次觉得这是一个公平的“妥协”,这个功能从未像我们在这里要求的那样得到实施和实施。

如果你同意请点赞

我不同意这需要一个全新的选项/配置。

我认为禁用严格选项时不应该是默认行为,但是当我启用严格选项BE STRICT并在较旧的项目中强制执行它们时。 启用严格是人们选择最严格/最准确的类型。 如果我启用了严格选项,我希望对我的代码有信心,这就是 TS 最好的部分。 但是,即使启用了严格,这些不准确的打字也令人头疼。

为了保持这个线程的可读性,我隐藏了一些对对话没有意义的切入点评论。 一般来说,您可以假设在前面的212 条评论中已经做出了以下评论,不需要重复:

  • TypeScript 应该第一次在检查行为中引入一个不切实际的重大突破性变化 - 不,我们以前没有这样做过,将来也不打算这样做,请更多地关注长期设计目标来这里倡导前所未有的变革之前的项目
  • 论据基于没有人考虑到配置标志存在这一事实的观点 - 我们知道将东西放在标志后面意味着它们不需要默认开启,这不是任何人的第一个牛仔竞技表演
  • 有什么好说的,赶紧动手吧! - 好吧,既然你这么说,我就相信了😕

这里有非常现实的实际和技术挑战需要解决,令人失望的是我们不能在这个线程中做有意义的工作,因为它充满了人们建议在标志后面的任何事情都没有真正的后果或任何错误可能be ts-ignore 'd 不是升级负担。

@瑞安卡瓦诺

谢谢你的总结。

在来这里倡导前所未有的变化之前,请更多地关注项目的长期设计目标

我已经删除了我的评论,如果您愿意,我也可以删除此评论。 但是如何理解项目的第一个目标:

1. 静态识别可能出错的结构。

(来源:https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals)

我不明白您引用的评论,因为未经检查的索引访问可能会导致运行时错误。 如果在 JavaScript 中你期待这样的事情,在 TypeScript 中你会得到危险和错误的信心。 错误的类型比any更糟糕。

好吧,既然你这么说,我相信了

当然,您是核心维护者之一,感谢您和团队以及 ts 的贡献者。 但这不再只是关于你。 比较赞成票:365 与反对票:6! 只有 6 个! 它显示了对类型安全的巨大需求。

但让我们谈谈解决方案。 团队是否有任何解决方案会发生或可以想到?

在这种情况下,您能否详细说明ts-ignore有什么问题? 我的意思是它可以使用codemod 之类的工具实现自动化,并且不会改变当前代码的行为。 当然,这不是很好的解决方法,但是,这是在没有标志的情况下引入此更改的一种可能方法。

您如何看待 ts 补丁版本的自动发布,例如4.0.0-breaking ? 它引入了一些(很多?)解决冲突的工作,但它允许每个人测试更改并准备代码库(不仅针对此功能请求)。 这可以在有限的时间段内完成,例如 3-6 个月。 我们将是第一个使用此版本的人。

了。 比较赞成票:365 与反对票:6! 只有 6 个! 它显示了对类型安全的巨大需求。
- @Bessonov

哈哈。
并不是说我同意@RyanCavanaugh的整个帖子......但是......来吧......
有什么百万? TS用户在那里?!? 365 想要此功能足以在此线程中发表评论和投票...

github 没有通知,但是对于尝试使用此问题中未定义索引的每个人,请查看我的评论上方的 PR 草案。

@RyanCavanaugh非常感谢你给我们一个玩它的机会。 我在一个非常小的(~105 个 ts(x) 文件)代码库上运行它并稍微玩了一下。 我没有发现任何重要的问题。 从以下更改的一行:

const refHtml = useRef(useMemo(() => document.getElementsByTagName('html')[0], []))

到:

const refHtml = useRef(useMemo(() => document.getElementsByTagName('html')[0] ?? null, []))

下周我将在一个中等项目上尝试它。

@lonewarrior556是 60 倍,如果您按赞数排序,它会在第一页上:)

@Bessonov我认为您应该在 Typescript 编译器代码库上尝试一下,由于 for 循环的非平凡使用,它可能会导致大量损坏。

您甚至可以使用标志来选择加入/退出以提供向后兼容性。

您添加的每个标志都是另一个需要测试和支持的配置矩阵。 出于这个原因,TypeScript 希望将标志保持在最低限度。

@MatthiasKunnen如果这是一些小的边缘情况功能,那么我同意这是不为此添加标志的可行理由。 但是直接数组访问经常出现在代码中,它既是类型系统中的一个明显漏洞,也是一个需要修复的重大更改,所以我觉得有必要设置一个标志。

标志是错误的方法,部分原因是组合
问题。 我们应该把这个 TypeScript v5 称为...那种方法
将可测试组合的数量从 2^N 变为仅 N...

2020 年 8 月 15 日星期六 15:56,Michael Burkman通知@ github.com
写道:

@MatthiasKunnen https://github.com/MatthiasKunnen如果这是一些
小的边缘案例功能,那么我同意这是一个不这样做的可行理由
为此添加一个标志。 但是直接数组访问显示在代码中
经常,并且既是类型系统中的一个明显漏洞,也是一个
中断更改以修复,所以我觉得有必要设置一个标志。


您收到此消息是因为您订阅了此线程。
直接回复本邮件,在GitHub上查看
https://github.com/microsoft/TypeScript/issues/13778#issuecomment-674408067
或取消订阅
https://github.com/notifications/unsubscribe-auth/AAADY5AXEY65S5HDGNGIPZDSA2O3FANCNFSM4C6KEKAA
.

刚刚有一个意外的未定义错误使数百个用户的应用程序瘫痪,如果这种类型检查到位,这个错误就会在构建时被捕获。 TypeScript 一直是我们交付更可靠软件的绝佳方式,但它的巨大效用正被这一单一遗漏所削弱。

接下来的 10 位想在这里发表评论的人如何测试 PR #39560 草案呢?

如果你想启用 ESLint 的@typescript-eslint/no-unnecessary-condition规则,这很烦人,因为它会抱怨所有的实例

if (some_array[i] === undefined) {

它认为这是一个不必要的条件(因为 Typescript 说它是!)但事实并非如此。 我真的不想在我的代码库中添加// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition

如果正确修复这个对于懒惰的 Javascript 程序员来说太过分了,也许我们可以有一个替代的数组访问语法来添加undefined ,例如

if (some_array[?i] === undefined) {

(随机语法建议;欢迎替代)

@Timmmm请阅读略高于所做的一个注释。 您可以尝试一个版本来实现此建议的一个版本——如果它解决了您的eslint问题,您可以在那里告诉我们。

@the0rted我确实阅读了该评论。 它没有实现我的建议的一个版本。 也许你只是应该再读一遍。

抱歉,当我说“这个”建议时,我指的是 OP 中的功能,即将array_of_T[i]视为T | undefined 。 我确实看到您在询问将特定索引运算符标记为“可能未定义”的语法,但是如果您使用 Ryan 的 PR 中的实现,则不需要它,因为所有索引运算符都将是“可能未定义” . 那不能满足你的需要吗?

是的 - 如果/当它降落时,我肯定会使用它。 但我从这次讨论中得到的印象是,很多人都反对它,因为它添加了一个额外的标志并使代码看起来应该更复杂(简单的 for 循环示例)。

所以我想提供一个替代建议,但显然人们讨厌它。 :-/

感谢所有善意的输入,不要以错误的方式@Timmmm。 我认为downvotes 只传达了这在这一点上已经在这个线程中被广泛使用。

对我来说,添加一个全面提高类型安全性的标志比引入新的选择加入语法的成本要低得多。

我上面没有提到它,但是一次性覆盖的语法已经存在: if ((some_array[i] as MyType | undefined) === undefined) 。 它不是那样简单作为一个新的速记,但希望这不是你必须经常使用的构造。

_Originally发表@osyrisrblxhttps://github.com/microsoft/TypeScript/issues/40435#issuecomment -690017567_

虽然这似乎是一个更安全的选择,但在您 100% 确定它始终已定义的极少数情况下,最好使用单独的语法选择退出此行为。

也许!:可以断言始终定义?

interface X {
    [index: string]!: number; // -> number
}

interface Y {
    [index: string]: number; // -> number | undefined
}

该建议错误地描述了问题。 问题不在于 TypeScript 对类型本身的理解。 问题来自 _accessing_ 数据,这是本线程前面已经提出的新语法。

你可以在 TypeScript 4.1 测试版中尝试这个,很快就会推出™(或者如果你现在需要的话,今晚晚上!🙂)

抱歉,如果以前提供过,但可以支持{[index: string]?: number} // -> number | undefined吗? 它与接口可选属性语法一致 - 默认情况下需要,可能未定义为“?”。

4.1 中的编译器选项很酷,但有更精细的控制也会很好。

如果你想在你的 repo 中尝试一下(花了我一点时间来弄清楚):

  1. 安装typescript@next yarn (add|upgrade) typescript@next
  2. 添加标志(对我来说在 tsconfig.json 中) "noUncheckedIndexedAccess": true

在我的项目中启用此规则的过程中,我遇到了这个有趣的错误:

type MyRecord = { a: number; b: string };

declare const myRecord: MyRecord;

declare const key: 'a' | 'b';
const value = myRecord[key]; // string | number ✅

// ❌ Unexpected error
// Type 'MyRecord[Key] | undefined' is not assignable to type 'MyRecord[Key]'
const fn = <Key extends keyof MyRecord>(key: Key): MyRecord[Key] => myRecord[key];

在这种情况下,我没想到myRecord[key]会返回类型MyRecord[Key] | undefined ,因为key被限制为keyof MyRecord

更新:提交问题https://github.com/microsoft/TypeScript/issues/40666

不过,这可能是一个错误/疏忽!

在我的项目中启用此规则的过程中,我遇到了这个有趣的错误:

type MyRecord = { a: number; b: string };

declare const myRecord: MyRecord;

declare const key: 'a' | 'b';
const value = myRecord[key]; // string | number ✅

// ❌ Unexpected error
// Type 'MyRecord[Key] | undefined' is not assignable to type 'MyRecord[Key]'
const fn = <Key extends keyof MyRecord>(key: Key): MyRecord[Key] => myRecord[key];

在这种情况下,我没想到myRecord[key]会返回类型MyRecord[Key] | undefined ,因为key被限制为keyof MyRecord

我会说这是一个错误。 基本上,如果keyof Type仅包含字符串/数字文字类型(而不是实际包含stringnumber ),则Type[Key] where Key extends keyof Type我认为不应该包括undefined

交叉链接到#13195,它还研究了“这里没有财产”和“这里有财产但它是undefined ”之间的异同

在此处跟踪与设计限制相关的一些问题

  • #41612

仅供参考:多亏了这个标志,我在我的代码库中发现了两个错误,非常感谢!
有点烦人的是,检查arr.length > 0不足以保护arr[0] ,但与额外的安全性 IMO 相比,这是一个小不便(我可以重写检查以使 tsc 高兴)。

@rubenlg我同意arr.length 。 为了检查第一个元素,我重写了我们的代码,例如

const firstEl = arr[0];
if (firstEl !== undefined) {
  ...
}

但是有几个地方我们做了if (arr.length > 2)或者更多,就有点别扭了。 但是我认为检查.length无论如何都不会是完全类型安全的,因为您可以修改它:

const a: number[] = [];

a.length = 1;

if (a.length > 0) {
    const b: number = a[0];
    console.log(b);
}

打印undefined

但是我认为检查.length无论如何都不会是完全类型安全的,因为您可以修改它:

const a: number[] = [];

a.length = 1;

if (a.length > 0) {
    const b: number = a[0];
    console.log(b);
}

打印undefined

这本质上是一个稀疏数组,超出了这类事情的范围。 总有一些非标准或偷偷摸摸的事情可以绕过类型检查器。

总有一种方法可以使先前的假设无效,这就是问题所在。 分析每个可能的执行路径成本太高。

const a: number[] = [1]
if (a.length > 0) {
    a.pop();
    console.log(a[0])
}
此页面是否有帮助?
0 / 5 - 0 等级