首页 > 解决方案 > 如何在 Typescript 中访问可能的空对象的对象属性?

问题描述

我有 2 个类似的设置,我希望它们产生相同的结果,但第二个会引发 Typescript 错误:

第一的:

interface ISample {
  requiredProp: string
  optionalProp?: string
}

const sampleObj: ISample = { requiredProp: "hello" }

const { requiredProp, optionalProp = "default" } = sampleObj

console.log(requiredProp, optionalProp)

// Expected Output: "hello", "default"
// Actual Output:   "hello", "default"

第二:

interface ISample {
  requiredProp: string
  optionalProp?: string
}

const sampleObj: ISample | {} = { requiredProp: "hello" } // object can now be empty

const { requiredProp = "default", optionalProp = "default" } = sampleObj

console.log(requiredProp, optionalProp)

// Expected Output: "hello", "default"
// Actual Output:   Error: Property 'requiredProp' does not exist on type '{} | ISample'. Property 'optionalProp' does not exist on type '{} | ISample'.

是的,谢谢打字稿我知道这些道具可能不存在于那个空对象上,这就是为什么我立即在同一行上为它们分配默认值...

我需要使对象可选地为空,并且仍然能够从中解构未定义的值,因此我可以为它们分配默认值(如上面的示例中所示)或将它们与可选的链接运算符一起使用。如何在不键入任何对象的情况下摆脱这个无用的错误?

标签: typescript

解决方案


简而言之,您无法访问ISample属性,sampleObj因为 TypeScript 无法推断性{}地区分联合成员,ISample | {}因为两个联合成员都对分配感到满意。因此,您不能访问一个属性,sampleObj除非它存在于联合的所有成员上(排除所有属性)。

工会

来自手册

联合类型是由两种或多种其他类型组成的类型,表示可能是这些类型中的任何一种的值。我们将这些类型中的每一种都称为工会的成员

请记住,联合不组合类型(这是交集的作用);它们为您提供了一个事物(变量、参数、属性等)在 assignment中可能存在的选项。例如:

// foo, by annotation, might be a string or a number...
let foo: string | number = 'bar';
// ... but it is assigned to a string, so its realized type is string:
foo.toUpperCase(); // ok, because foo: string

// We can reassign foo as long as it is still a member of the original annotated union
foo = 0;
foo.toUpperCase(); // error, because foo: number
//  ^^^^^^^^^^^ Property 'toUpperCase' does not exist on type 'number'

这里基于分配foo推断出更窄的类型(或者更确切地说,工会成员string被歧视)。

再次强调,歧视工会成员(在分配或其他情况下)不允许这些成员,直到重新分配,即你不能这样做:

let foo: { bar: string } | { baz: number } = { bar: 'foobar' }
foo.baz = 0
//  ^^^ Property 'baz' does not exist on type '{ bar: string; }'.
foo = { baz: 0 } // ok

空的类型{}

键入事物 as{}可能会导致一些出乎意料但完全有效的行为

...空类型似乎违背预期:

class Empty {}
 
function fn(arg: Empty) {
  // do something?
}
 
// No error, but this isn't an 'Empty' ?
fn({ k: 10 });

考虑以下有效的 TypeScript:

let foo: {} = {};
foo = { bar: 0 };
foo = 'bar';
foo = [ true, 0, '' ];

这些都是有效的,因为每个属性{}都存在于这些实际类型中的每一个上。如果我们改变它:

  let foo: { length: number } = { length: 0 };
  const o = { bar: 0 }
  foo = o;
//^^^ Property 'length' is missing in type '{ bar: number; }' 
//      but required in type '{ length: number; }'
  foo = 'bar';
  foo = [ true, 0, '' ];

在这里,除了其中一个之外,所有这些都满足length: number{}没有要满足的属性,所以任何事情都会发生(除了nulland undefined)。

另请注意,如果{}没有类型断言,该类型实际上是无用的:

let foo: {} = 'bar';
foo.length;
//  ^^^^^^ Property 'length' does not exist on type '{}'

结论/答案

因此,再次查看发布的示例:

interface ISample {
  requiredProp: string
  optionalProp?: string
}

const sampleObj: ISample | {} = { requiredProp: "hello" } // object can now be empty

const { requiredProp = "default", optionalProp = "default" } = sampleObj
//      ^^^^^^^^^^^^              ^^^^^^^^^^^^
// Property 'requiredProp' does not exist on type '{} | ISample'
// Property 'optionalProp' does not exist on type '{} | ISample'.

console.log(requiredProp, optionalProp)

在分配 的初始值时sampleObj,TypeScript 尝试通过区分与给定值不匹配的成员来缩小类型。在这种情况下,它不能排除任何工会成员,因为{ requiredProp: "hello" }同时满足ISample{}


为了彻底起见,请注意const { requiredProp = "default", optionalProp = "default" } = sampleObj正在尝试访问sampleObj.requiredPropand sampleObj.optionalProp,它不能存在于 type 上{} | ISample。您的选择是:

  • 更改ISample,以便它可以允许一个空对象(通过使所有属性可选)。
  • 更改sampleObj要使用的类型,在不更改定义Partial<ISample>的情况下完成相同的事情。ISample
  • 始终将键入的事物的所需属性分配ISample给默认值。

无论您的选择如何,您都应该{}从联合中删除以避免头痛(尽管您可能会查看类型谓词)。


推荐阅读