TypeScript 空合并运算符应该始终包含右操作数的类型,

lymgl2op  于 9个月前  发布在  TypeScript
关注(0)|答案(8)|浏览(116)

搜索词:
代码

一个玩具示例是这样的。

  1. let foo: string = "";
  2. foo = "bar" ?? 123;

这个显然没问题,因为 "bar" 总是为真。
然而,当你考虑在使用 Record 对象之前检查它们的真值时,这就变得有点问题了。

  1. const elts = ["foo", "bar", "spam", "spam", "foo", "eggs"];
  2. const counts: Record<string, number>;
  3. for (const elt of elts) {
  4. // This really **should** raise an error.
  5. counts[elt] = counts[elt] ?? "zero";
  6. counts[elt] += 1;
  7. }

预期行为:

应该引发错误。

实际行为:

奇怪的是,在严格模式下没有引发错误,但在非严格模式下引发了错误。

  1. $ ./node_modules/.bin/tsc ./foo.ts
  2. foo.ts:5:3 - error TS2322: Type 'number | "zero"' is not assignable to type 'number'.
  3. Type '"zero"' is not assignable to type 'number'.
  4. 5 counts[elt] = counts[elt] ?? "zero";
  5. ~~~~~~~~~~~
  6. Found 1 error.
  1. $ ./node_modules/.bin/tsc --strict ./foo.ts
  2. # No error, exits 0 and emits JS.

Playground链接:

Playground链接
切换 strictNullChecks 配置选项将显示问题。

相关问题:

qxsslcnc

qxsslcnc1#

这可能与或重复的 #13778 有关。
--strictNullChecks 为真时,编译器假定 counts[elt] 必须是类型 number,而它永远不会是 nullundefined。如果你想让编译器注意到 counts[elt] 可能为 undefined,那么你需要(目前)手动将 undefined 包含在属性类型中:

  1. const counts: Record<string, number | undefined> = {};
  2. for (const elt of elts) {
  3. counts[elt] = (counts[elt] ?? "zero"); // error now
  4. counts[elt] = (counts[elt] ?? 0) + 1; // okay
  5. }

Playground
--strictNullChecks 为假时,编译器从不假定空合并会短路,因为它永远无法消除 nullundefined。请参阅此 PR 中的评论。

hk8txs48

hk8txs482#

看起来这种事情可能存在一些“右结合性”(我不确定正确的术语来描述我的意思)。
我的意思基本上是这样的:

  1. string -> string
  2. string ?? number -> string | number;
  3. null ?? number -> number;

我认为算法大致如下:

  • 给定一个链 A ?? B ?? C ?? ...,取第一个对(空合并是左结合的,对吧?)
  • 解析 AB 的类型
  • 判断 A ?? B 的类型为 NonNull<A> | B
  • 递归
  1. string ?? (string | null) ?? string[]
  2. // Take first pair...
  3. => (NonNull<string> | (string | null)) ?? string[]
  4. // Simplify the types...
  5. => (string | null) ?? string[]
  6. // Recursively take first pair...
  7. => NonNull<string | null> | string[]
  8. // Simplify the types...
  9. => string | string[]
  10. // Done!
展开查看全部
w8biq8rn

w8biq8rn3#

&& , ||! 运算符也存在类似的问题。

omqzjyyz

omqzjyyz4#

我刚刚遇到了这个问题,但我认为这是按照预期工作的。重新表述一下@jcalz发现的问题,问题描述中的示例展示了一个过于宽松的 Record 类型:

  1. const counts: Record<string, number>;
  2. // strictNullChecks: true
  3. counts.foo // evaluated as (number)
  4. // strictNullChecks: false
  5. counts.foo // evaluated as (number | null | undefined)

在严格模式下,@typescript-eslint/no-unnecessary-condition 可以捕获 TS 在计算类型时的短路行为,并会告诉你:

  1. const elts = ['foo', 'bar', 'spam', 'spam', 'foo', 'eggs'];
  2. const counts: Record<string, number> = {};
  3. for (const elt of elts) {
  4. counts[elt] = counts[elt] ?? 'zero'; /*
  5. ^^^^^^^^^^^
  6. Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined. [eslint] */
  7. counts[elt] += 1;
  8. }

如果你明确地将类型扩大以包含 null | undefined,你将看到松散模式中的错误:

  1. const elts = ['foo', 'bar', 'spam', 'spam', 'foo', 'eggs'];
  2. const counts: Record<string, number> = {};
  3. for (const elt of elts) {
  4. counts[elt] = (counts[elt] as number | null | undefined) ?? 'zero'; /*
  5. ^^^^^^^^^^^
  6. Type 'number | "zero"' is not assignable to type 'number'. Type 'string' is not assignable to type 'number'. [ts] */
  7. counts[elt] += 1;
  8. }

考虑到一个 linter 能够指出潜在的意外行为,我不知道TS端是否有什么需要修复的地方。

展开查看全部
1u4esq0p

1u4esq0p5#

我尊重地不同意(显然 - 我创建了这个问题😄)。在TypeScript中使用过于宽松的Record类型是一种常见的习惯用法,我认为改变这种行为使得这种习惯用法更容易使用。

drnojrws

drnojrws6#

我认为以下几点可能与此问题有关:

  1. function auto<T1, T2>(value: T1, fallback?: T2) {
  2. return value ?? fallback;
  3. }
  4. // Incorrectly typed to "unknown" even though the right hand path of `??` will never be taken in this case.
  5. const f1 = auto(42);
  6. function forced<T1, T2>(value: T1, fallback?: T2): T1 extends undefined | null ? NonNullable<T1> | T2 : T1 {
  7. return value ?? fallback as any;
  8. }
  9. // Correctly typed to "number".
  10. const f2 = forced(42);

这在你的系统依赖于备用值时可能是重要的。在当前的 ?? 类型下,系统会强制你定义一个 T2 备用值,因为如果你不定义一个,那么它的 unknown 类型从可选变为非可选,即使在 T1 永远不可能是 nullundefined 的情况下,也会污染最终的返回类型。
从我的Angular 来看,似乎 T1 ?? T2 的类型可以更精确地表示为 T1 extends undefined | null ? NonNullable<T1> | T2 : T1,而不是它当前的 NonNullable<T1> | T2 值,但我猜想我可能在理解上有所遗漏?

mfuanj7w

mfuanj7w7#

为了记录,这个:

  1. let foo: string = "";
  2. foo ??= 123;

具有"预期"的行为。

62lalag4

62lalag48#

似乎通过打开noUncheckedIndexedAccess配置来解决。

相关问题