如何有条件地向对象添加键?

Posted

技术标签:

【中文标题】如何有条件地向对象添加键?【英文标题】:How to conditionally add a key to an object? 【发布时间】:2021-06-16 22:29:42 【问题描述】:

考虑这些类型:

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?有办法解决吗?

【问题讨论】:

看起来你编辑了你的问题,这样做解决了你自己的问题! :) 【参考方案1】:

这是 TypeScript 的两个次要设计限制和一个主要设计限制的组合,您最好重构或使用 type assertion 继续前进。


首先是microsoft/TypeScript#30506。通常,检查对象的一个​​属性会缩小该属性的表观类型,但不会缩小对象本身的表观类型。唯一的例外是对象是discriminated union 类型并且您正在检查它的判别属性。在您的情况下,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 检查传播到b 时,这会阻止您的a.b 检查对观察到的a 类型产生任何影响。

您可以编写自己的自定义type guard function 来检查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 值的b 具有undefined b 的东西”,则任何类型的缩小行为都会依靠这种对等。感谢smarter union type checking support introduced in TS 3.5,编译器在某些具体情况下确实理解这种等价性,但它不会发生在类型级别。


即使我们将A 更改为EquivalentA 并将a.b 检查更改为isBString(a),您仍然会遇到错误。

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

这就是大问题:control flow analysis 的基本限制。

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

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

看来你期待的代码

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

工作,因为编译器应该执行如下分析:

a 的类型是 a: string, b: string | a: string, b?: undefined。 如果aa: string, b: string,那么(除非"" 值有任何奇怪之处),...a, a: 1, ...isBString(a) && b: Number(a.b) 将是a: number, b: number。 如果aa: string, b?: undefined,那么``...a, a: 1, ...isBString(a) && b: Number(ab) will be aa: number, b ?: 未定义` 因此,此表达式是一个联合 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 可能是string 类型的可能性,并且编译没有错误。

甚至更简单:

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

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

Playground link to code

【讨论】:

感谢您如此详尽详细的回答!【参考方案2】:

看起来您编辑了您的问题,从而解决了您自己的问题! :) 除了最终测试之外,我的代码与您的代码相同。


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


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

/* With more generic object types:
type A = 
  [id: string]: string;
;


type B = 
  [id: string]: number;
;
*/

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


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


console.assert(b.a === 1, 'b.a');
console.assert(b.b === 2, 'b.b');
console.log(b);

tsc temp.ts &amp;&amp; node temp.js 运行并输出:

 a: 1, b: 2 

【讨论】:

我也决定这样做。我在这里想了解的是,将 b 推断为 string | number | undefined 是否是预期的 TS 行为、编译器限制、错误或其他原因。 类型包含在问题的最开头,仅供参考。 已编辑,抱歉。错过了您的类型! 编辑后,我们的代码是一样的,并且工作正常,没有错误。我错过了什么? 嗯,我在 TS 操场上运行时遇到了同样的错误:typescriptlang.org/play?ts=4.2.3#code/…

以上是关于如何有条件地向对象添加键?的主要内容,如果未能解决你的问题,请参考以下文章

html 有条件地向一个属性添加值

如何循环遍历对象数组并根据 JavaScript 中的条件添加新的对象键?

JavaScript常用的开发小技巧-总结

JavaScript常用的开发小技巧-总结

在 Vue.js 中有条件地定位具有 CSS 样式的类

使用 Ramda 在特定条件下添加对象属性值