TS:有条件地向对象添加键

如何解决TS:有条件地向对象添加键?

开发过程中遇到TS:有条件地向对象添加键的问题如何解决?下面主要结合日常开发的经验,给出你关于TS:有条件地向对象添加键的解决方法建议,希望对你解决TS:有条件地向对象添加键有所启发或帮助;

问题描述

考虑这些类型:

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
// }

TS 抛出此错误

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?有没有办法解决这个问题

解决方法

这是 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 检查在您将其扩展到 a 时对观察到的 b 类型产生任何影响。

您可以编写自己的自定义 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 视为“具有 stringb 的事物, 具有 {{ 1}} undefined" 将依赖于这种等价性。多亏了 smarter union type checking support introduced in TS 3.5,编译器在某些具体情况下确实理解了这种等价性,但它不会发生在类型级别。


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

isBString(a)

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

编译器检查某些常用的句法结构,并尝试根据这些来缩小值的明显类型。这适用于 const stillBadB: B = { ...a,a: 1,...isBString(a) && { b: Number(a.b) } } // error! 语句等结构或 if|| 等逻辑运算符。但这些缩小的范围是有限的。对于 && 语句,这将是真/假代码块,而对于逻辑运算符,这将是运算符右侧的表达式。一旦离开这些范围,所有控制流的缩小都将被遗忘。

您不能将控制流缩小的结果“记录”到变量或其他表达式中并在以后使用它们。只是没有机制允许这种情况发生。请参阅 microsoft/TypeScript#12184 以获取允许此操作的建议;它被标记为“重访”。还有 microsoft/TypeScript#37224,它只对新的对象字面量提出要求。

看来你期待的代码

if

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

  • const b: B = { ...a,...isBString(a) && { b: Number(a.b) } } 的类型是 a
  • 如果 { a: string,b: string } | {a: string,b?: undefined}a,那么(除非 {a: string,b: string} 值有任何异常),"" 将是 {...a,...isBString(a) && {b: Number(a.b) }
  • 如果 {a: number,b: number}a,则 ``{...a,...isBString(a) && {b: Number(ab) }{a: string,b?: undefined} {a: number,b?: undefined}`
  • 因此这个表达式是一个联合 will be a,它可以赋值给 {a: number,b: number} | {a: number,b?: undefined}

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

这几乎肯定永远不会自动发生,因为编译器模拟联合类型的每个值在任何地方都有可能缩小的代价会高得令人望而却步。您甚至不能要求编译器明确地执行此操作(这就是 microsoft/TypeScript#25051 的内容)。

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

B

在这一点上,这真的太丑陋了,而且与您的原始代码相距甚远,令人难以置信。


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

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

这里我们要求编译器在我们将它扩展到新的对象字面量中时假装 const b: B = { ...(a as Omit<A,"b">),...a.b && { b: Number(a.b) } } // okay 甚至没有 a 属性。现在编译器甚至不考虑生成的 b 可能是 b 类型的可能性,并且编译没有错误。

或者更简单:

string

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

Playground link to code

,

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


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.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 && node temp.js 身份运行并输出:

{ a: 1,b: 2 }

编程问答问答

在 CSS 中设置 cellpadding 和 cellspacing?
如何在 Java 中创建内存泄漏?
浮点数被破坏了吗?
按字符串属性值对对象数组进行排序
如何加快Android模拟器的速度?
如何舍入至多 2 位小数?
使用 Git 版本控制查看文件的更改历史记录
如何在 JavaScript 中检查空/未定义/空字符串?
微信公众号搜索 “ 程序精选 ” ,选择关注!
微信公众号搜 "程序精选"关注