TypeScript Design Meeting Notes, 8/13/2024

mbzjlibv  于 23天前  发布在  TypeScript
关注(0)|答案(7)|浏览(18)

类字段和参数属性的顺序

#45995

  • 当我们调整类字段发射(使用 useDefineForClassFields )时,我们也调整了类字段和参数属性的相对顺序。
  • 在JS中,类字段在构造函数体之前运行。
  • 在TS中,以前,类字段在参数属性之后运行,但在构造函数体之前。
  • useDefineForClassFields 中,我们对参数属性的任何访问都出错,但除此之外还好。
  • 我们是否应该调整顺序以保持一致?
  • useDefineForClassFields 作为遗留行为引入,以避免破坏,为什么要破坏人们?
  • 可以将其设置为false进行弃用。
  • 我们可以运行一个始终出错的PR,看看会破坏什么。
  • 有没有让事情随着时间变得更符合规范的论据?
  • 有什么我们可以在emit上做的聪明事吗?
  • 可观察到的副作用。
  • 困难的部分是,大多数受GitHub运行影响的代码库都不会被发现。通常这些是更成熟的代码库。
  • 总是可以做代码修改。我们通常在这里依赖快速修复。
// Before
class A {
    x = 1;
    constructor(public y: number) {
        console.log(this.y);
    }
}
// After
class A {
    declare x;
    constructor(public y: number) {
        this.x = 1;
        console.log(this.y);
    }
}
  • 我们中的一些人觉得获得一致的emit是个好主意。选择未来的某个日期作为弃用并最终移除的计划。但需要先进行实验。
  • 也许6.0弃用就是计划。
  • 但这仅自ES2022以来可用!
  • 6.0弃用=6.5移除,距离现在还有大约两年的时间。
  • 另外:我们应该调查ECMAScript中的参数属性。
yzuktlbb

yzuktlbb1#

感谢您讨论这个问题!
原始问题突出显示了ES2021及以后的目标发射的不一致性。
这里的注解建议一个解决方案,即废弃设置useDefineForClassFields: false的能力。听起来很好。
字面意义上,这将为目标发射>= ES2022提供一致性,但会将所有较早的默认行为保留为遗留行为。
建议的弃用是否意味着进一步暗示useDefineForClassFields: true将成为普遍正确的?因为这将解决原始问题中提到的不一致性。(这是一个澄清性问题,不打算倡导特定的答案。)

xwmevbvl

xwmevbvl2#

如果参数属性可能会被弃用,是否有一个关于这个想法的规范参考?它是否与诸如命名空间和枚举等其他非标准运行时特性(除了它们与 https://github.com/rbuckton/proposal-enum 的重叠部分)有任何关系?
我之所以提出这个问题,是因为在关于 Node.js 最近的 --experimental-transform-types PR 的讨论中,作者担心不支持这些运行时特性会创造出一种新的 TypeScript 类型。如果未来的 TypeScript 版本也不包含它们,那么 TypeScript - 运行时特性就不是一种新的类型,而是一种未来的类型。对于没有向后兼容性约束的新工具来说,能够针对这种类型进行目标定位可能会有所帮助。

ki0zmccv

ki0zmccv3#

如果参数属性可能会被弃用,那么这个想法有没有一个规范的参考?
参数属性没有被弃用。我们正在考虑弃用的是 useDefineForClassFields: false ,这也正好解决了初始化顺序问题。
我们对将来将参数属性作为TC39提案进行调查表示有兴趣。

q0qdq0h2

q0qdq0h25#

建议的弃用是否意味着 useDefineForClassFields: true 会变得更普遍?
是的,这将是意图。需要注意的是,即使在 useDefineForClassFields: true 下,你也可以通过使用一个 declare 字段并将初始化器移动到构造函数体中来近似 useDefineForClassFields: false ,因此对于特定情况仍然有一个解决方法。

nszi6y05

nszi6y056#

我必须说我很震惊!这所有的内容都像是你愿意打破很多代码库一样!请看下面的反馈,这是我从一个TS强用户的Angular 出发的,他一直在一个大(且私有的)Angular驱动的代码库(由瑞士一家保险公司拥有)工作,该代码库从TS 2.x开始。
在我的反馈中,我会提出这样的观点:为什么你最近进行的 experiment 不应该是指导性的,因为这些数字完全是错误的。
在 useDefineForClassFields 下,我们对访问参数属性时的任何错误都会报错,但除此之外一切都很好。
我想你是在提到 #55028 吧?

不,这并不好,因为立即执行的函数不会被编译器捕获,参见我在这里的“最后备注”:#55132 (评论)

例如,这会影响到使用 NgRx 和 Effects 的任何 Angular 项目,根据文档,它们会监听 actions$ 可观察对象(通过参数属性注入)。参见 https://ngrx.io/guide/effects#writing-effects
这些错误不会被 TS 编译器捕获,只会在运行时出现。这就是你的 experiment 不准确的第一个原因。
当我们调整我们的类字段发射(使用 useDefineForClassFields)时,我们也调整了类字段和参数属性的相对顺序。
对于许多使用 构造函数依赖注入 的项目来说,这是一个巨大的破坏性变化,如果他们想要在示例化期间访问注入的服务(这是与 RxJS 和/或 NgRx 以及我相信许多其他库常见的做法),那么最常见的情况下就是避免每次调用时返回不同对象标识的方法。相反,通常希望在示例化期间分配一个单一的固定对象(例如可观察对象),这样就不会干扰变更检测。
通过改变顺序(并计划淘汰任何回到旧顺序的方法,除了一些古怪的解决方法,参见下面),你实际上是在废弃 构造函数依赖注入 ,因为 Angular(例如)从一开始就使用了它。
这让我想到了第二个原因:为什么你的 experiment 不准确:
在你的研究中(如果我没记错的话),你只是查看了Github上的项目。这些距离一个现实且准确的统计样本非常远,原因如下:
1.大多数企业代码库通常是私有的——你在Github上找不到它们。
1.我想绝大多数受影响的项目都是使用 构造函数依赖注入 和参数属性的单页应用程序。那些将是私有(企业)项目,而不是托管在Github上的项目。
1.相比之下,在Github上,大多数项目(尤其是你想在你的研究中查看的前1000个项目)更可能是某种类型的库,它们不使用 构造函数依赖注入 与“示例化时访问”结合在一起。
1.即使这些项目过去曾经受到影响(这个bug已经存在了两年半了),它们很可能已经采用了他们的代码来解决这个破坏性的变化(因为前1000个开源项目可以被认为是维护良好的)。所以看看这些(前1000个)维护良好的项目现在不会告诉你关于有多少不受良好维护的项目会受到影响的信息。
现在可能会有人问:既然有这么多私有的企业项目可能会遇到问题:为什么我们没有看到更多的人参与报告这个问题?我可以想到两个原因:
1.我认为大多数私有企业代码库在转向 target: es2022 或更新版本时,其 tsconfig.json 切换并不是那么好维护。要么是因为粗心大意、缺乏资源、缺乏重要性("ES2021及以下将继续运行,为什么要改变呢?"),或者是因为旧的目标仍然需要支持较旧的环境。
1.那些已经做出改变的项目,可能只是通过使用 useDefineForClassFields=false 来解决排序问题。
useDefineForClassFields 是作为遗留行为引入的,以避免破坏,为什么要破坏人们呢?
可以将其设置为 false 以弃用。

不,我的看法不同:它被引入(与 TS 3.7,2019年11月)作为让人们遵守即将到来的标准的准备手段。它原本是为用户主动将其设置为 true (默认为 false)。此时的用户不会想到执行顺序会被以一种破坏性的方式改变。从 target: es2022 (introduced Nov 2021) 开始,它被赋予了额外的语义:突然间,将它明确地设置为 false 会恢复旧的行为。用户默默地接受了这一点:由于由此产生的错误,例如 Angular 团队曾经在他们的 useDefineForClassFields=false 生成的 tsconfig.json 中添加 @angular/cli (直到版本 17,直到 18 个月前才完全停止添加它)(在我看来,这是一个糟糕的决定,更多关于我在下面最后的评论中的想法)。在他们负责构建 Angular 应用程序(或库)的内部工具 ng new 中,他们甚至会修补用户的 tsconfig.json 以便能够在其中使用 @angular/cli (只有在目标缺失或低于 es2022 时才会进行修补)。为了做到这一点,他们还在用户的 tsconfig.json 中静默地添加了 target: es2022,以解决 emit 顺序破坏的问题(参见webpack工具和esbuild工具-在这两个文件中,他们甚至提到了根本问题 #45995 )。

除此之外:没错!为什么要破坏人们?为什么要破坏依赖于“参数属性”首先执行、类字段其次、构造函数体第三的我的“遗留”代码库?我们总是可以进行代码修改。

// Before
class A {
    x = 1;
    constructor(public y: number) {
        console.log(this.y);
    }
}
// After
class A {
    declare x;
    constructor(public y: number) {
        this.x = 1;
        console.log(this.y);
    }
}

我不确定。这是你为旧代码库解决这个破坏性的移除 useDefineForClassFields=false 的方法吗?如果是的话:你真的认为这是一种好的开发者体验(DX)吗?这个解决方案可能适合与由 Set 语义引起的 bug 相关的罕见情况。然而,如果你想解决更频繁发生的顺序问题,那么它并不适用。
我们中的一些人觉得一致的 emit 是件好事。选择一个未来的日期来弃用并最终删除是提议的计划。但需要先进行实验。

这里有一个远比 DX 更友好的替代方案:

  • 请重新考虑 TS是否让许多项目走上了一条道路,即它们期望参数属性首先出现,而类字段其次。现在你们似乎并不这么认为。但我是这样认为的,原因很简单: 构造函数依赖注入 由 Angular 推广
  • 如果你真的打算继续支持参数属性,请接受这样一个事实:它们必须首先执行,否则 构造函数依赖注入 将失去很多 DX:基本注入仍然有效,但“访问示例化时”将不再可能,除非通过奇怪的方法在构造函数中实现(结合单独的 declare 行),而不是在单行字段声明中实现。顺便说一下,这种方法击败了你试图实现的一件事:让用户能够使用 ES 类字段(我支持这一点,但我想使用它们,并且能够用通过依赖注入注入到构造函数中的事物来初始化它们)。
  • 一个向前迈进的好方法可能是:
  • 当参数属性未使用时一切都很好。那时就不需要改变任何东西。
  • 一旦参数属性被用户明确要求使用此功能,提供“旧”顺序就变得比新顺序更重要了。实际上,参数属性是一个如此伟大的特性(至少在最近之前),应该被认为是值得尝试并纳入 ES 标准的功能之一。如果你这样看待它,那么打破“新” ES 类字段的 emit 并继续让它们首先执行是可以接受的,只让 ES 类字段排在第二位。

最后的意见:

Angular 在某种程度上推广了新的 inject() 方法 since v14.2 作为 Constructor Dependency Injection 的替代方案。他们并没有大力推广它,但现在他们在示例中使用了它而不是 Constructor Dependency Injection 的方式,可能是因为示例更简洁,因此更容易理解。在我看来,他们似乎愿意放弃对 Constructor Dependency Injection的支持(尽管没有人正式说过这一点-这就是为什么我也认为他们可能甚至没有意识到这些问题本身的原因,这太奇怪了!)。在我的理解中,有三个原因使得 inject() 以这种方式被推广:

  1. 他们想要摆脱基于类的路由守卫(以及类似的东西),从 Angular@14.2(并允许用户使用更简单的功能路由守卫)

  2. (这只是我的一个假设)他们可能担心在发射顺序中的破坏性变化可能无法得到很好的解决(即通过恢复旧的行为) - 他们肯定意识到了破坏性变化,上面的代码证明了这一点(请参阅上面提到的webpack-tooling和esbuild-tooling链接)。

  3. 他们可能担心,提案ECMAScript Decorators for Class Method and Constructor Parameters可能不会成功,因此他们可能会尽快为他们的构造函数依赖注入装饰器(@Inject、@self、@host、@SkipSelf、@optional)使用TS支持experimentalDecorators。@rbuckton,也许你可以与Angular团队讨论以澄清这一点?

如果没有什么改变(即如果你坚持描述的废弃useDefineForClassFields=false的计划),我可以90%确定地说:我们今天所知道的构造函数依赖注入将会消失。这将是非常令人伤心的,因为它相对于基于inject()的依赖注入有优点和缺点(在我看来,优点远远大于缺点)。如果你感兴趣,我可以写更多关于优点和缺点的内容,但我认为现在这对讨论来说并不是那么重要。

7fhtutme

7fhtutme7#

为了在类字段初始化器之前执行参数属性,所有类字段初始化都必须转换为在由参数属性诱导的字段设置之后在构造函数中完成。这对于装饰器来说是可以的,只要装饰器降级,但是移除初始化表达式将对原生装饰器可见,因此使用参数属性意味着永远降级装饰器。由于使用了参数属性,这些是很大的变更。
如果参数属性得到标准化,不能保证它们与TypeScript相比具有相同的类字段排序。

相关问题