首页 > 解决方案 > 如何有条件地向对象添加键?

问题描述

考虑以下类型:

type A = {
  a: string;
  b?: string;
}

type B = {
  a: number;
  b?: number;
}

我想通过覆盖一些键并根据原始对象是否具有它们有条件地添加键来将类型对象A转换为:B

const a: A = {
  a: '1',
  b: '2'
}

const b: B = {
  ...a,
  a: 1,
  ... a.b && {b: Number(a.b)}
}

// expected:
// const b: B = {
//   a: 1,
//   b: 2
// }

TypeScript 抛出此错误:

Type '{ b?: string | number | undefined; a: number; }' is not assignable to type 'B'.
  Types of property 'b' are incompatible.
    Type 'string | number | undefined' is not assignable to type 'number | undefined'.
      Type 'string' is not assignable to type 'number | undefined'.

为什么会这样推断b?有没有办法解决它?

标签: typescripttype-inferencespread-syntax

解决方案


它是 TypeScript 的两个次要设计限制和一个主要设计限制的组合,您最好重构或使用类型断言来继续前进。


首先是microsoft/TypeScript#3050​​6。通常,检查对象的一个​​属性会缩小该属性的表观类型,但不会缩小对象本身的表观类型。唯一的例外是对象是可区分的联合类型并且您正在检查其区分属性。在你的情况下,A不是一个受歧视的工会(它根本不是一个工会),所以这不会发生。观察:

type A = {
  a: string;
  b?: string;
}
declare const a: A;
if (a.b) {
  a.b.toUpperCase(); // okay
  const doesNotNarrowParentObject: { b: string } = a; // error
}

在microsoft/TypeScript#42384有一个更新的开放请求来解决这个限制。但是现在,无论如何,这可以防止您的a.b检查对a您将其传播到b.

您可以编写自己的自定义类型保护函数来检查a.b和缩小类型a

function isBString(a: A): a is { a: string, b: string } {
  return !!a.b;
}
if (isBString(a)) {
  a.b.toUpperCase(); // okay
  const alsoOkay: { b: string } = a; // okay now
}

下一个问题是编译器看不到一个对象,其属性是一个联合,它等同于一个对象联合:

type EquivalentA =
  { a: string, b: string } |
  { a: string, b?: undefined }

var a: A;
var a: EquivalentA; // error! 
// Subsequent variable declarations must have the same type.

a编译器认为“带有string-valuedb的东西带有 a 的东西”的任何类型的缩小行为undefined b都将依赖于这种等价性。由于TS 3.5 中引入了更智能的联合类型检查支持,编译器在某些具体情况下确实理解这种等价性,但它不会发生在类型级别。


即使我们更改AEquivalentA并将a.b检查更改为isBString(a),但您仍然有错误。

const stillBadB: B = {
  ...a,
  a: 1,
  ...isBString(a) && { b: Number(a.b) }
} // error!

这就是大问题:控制流分析的基本限制。

编译器检查某些常用的句法结构,并尝试根据这些来缩小明显的值类型。这适用于诸如语句之类的结构或诸如orif之类的逻辑运算符。但这些缩小的范围是有限的。对于语句,这将是真/假代码块,而对于逻辑运算符,这是运算符右侧的表达式。一旦你离开了这些范围,所有的控制流变窄都被遗忘了。||&&if

您不能将控制流缩小的结果“记录”到变量或其他表达式中并在以后使用它们。只是没有机制允许这种情况发生。(有关允许这样做的建议,请参阅microsoft/TypeScript#12184 ;它被标记为 TS4.4 的“重新访问”更新,此问题已通过新的控制流分析功能修复,但此修复对当前代码没有任何帮助,所以我不会进入它)。请参阅microsoft/TypeScript#37224,它要求在新的对象文字上对此提供支持。

看来您期望代码

const b: B = {
  ...a,
  a: 1,
  ...isBString(a) && { b: Number(a.b) }
} 

工作,因为编译器应该执行类似下面的分析:

  • 的类型a{ a: string, b: string } | {a: string, b?: undefined}
  • 如果a是,那么(除非有任何带有虚假值的{a: string, b: string}怪异),将是 a 。""{...a, a: 1, ...isBString(a) && {b: Number(a.b) }{a: number, b: number}
  • 如果a{a: string, b?: undefined},那么``{...a, a: 1, ...isBString(a) && {b: Number(ab) } will be a{a: number, b?: undefined}`
  • 因此,这个表达式是一个{a: number, b: number} | {a: number, b?: undefined}可分配给的并集B

但这不会发生。编译器不会多次查看同一个代码块,想象某个值已被依次缩小到每个可能的联合成员,然后将结果收集到一个新的联合中。也就是说,它不执行我所说的分布式控制流分析;请参阅microsoft/TypeScript#25051

这几乎可以肯定永远不会自动发生,因为编译器要模拟联合类型的每个值在任何地方都具有所有可能的变窄,这将是非常昂贵的。您甚至不能要求编译器明确地执行此操作(这就是 microsoft/TypeScript#25051 的目的)。

让控制流分析多次发生的唯一方法是给它多个代码块:

const b: B = isBString(a) ? {
  ...a,
  a: 1,
  ...true && { b: Number(a.b) }
} : {
    ...a,
    a: 1,
    // ...false && { b: Number(a.b) } // comment this out
    //  because the compiler knows it's bogus
  }

在这一点上,这真的太丑陋并且与您的原始代码相去甚远,以至于不可信。


正如另一个答案所提到的,您可以完全使用不同的工作流程。或者你可以在某处使用类型断言来让编译器满意。例如:

const b: B = {
  ...(a as Omit<A, "b">),
  a: 1,
  ...a.b && { b: Number(a.b) }
} // okay

在这里,我们要求编译器在将其传播到新的对象字面量中时假装它a甚至没有属性。b现在编译器甚至不考虑结果b可能是 type的可能性string,并且它编译没有错误。

或者更简单:

const b = {
  ...a,
  a: 1,
  ...a.b && { b: Number(a.b) }
} as B

在这种情况下,编译器无法验证您确信它是安全的东西的类型安全,类型断言是合理的。这会将这种安全性的责任从编译器转移到您身上,所以要小心。

Playground 代码链接


推荐阅读