基于嵌套对象内部属性的Typescript联合类型

5
我正在尝试基于对象中的嵌套属性创建联合类型。请参见下面的示例:
type Foo = {
    abilities: {
        canManage: boolean
    }
}

type Bar = {
    abilities: {
        canManage: boolean
    }
    extraProp: number
}

type Condition1 = {
    abilities: {
        canManage: true
    }
} & Bar

type Condition2 = {
    abilities: {
        canManage: false
    }
} & Foo

type TotalData = Condition1 | Condition2

const data: TotalData = {
    abilities: {
        canManage: false, // if canManage is false, TS should complain when I add the `extraProp` key
    },
    extraProp: 5
}

我遇到的问题是 TypeScript 忽略了我设置的条件。如果 canManage 值为 true,我只想允许特定的属性。但是当嵌套时似乎无法正常工作。但如果没有嵌套,只有简单的内容,那就没问题:

type Foo = {
     canManage: boolean
}

type Bar = {
    canManage: boolean
    extraProp: number
}

type Condition1 = {
    canManage: true
} & Bar

type Condition2 = {
    canManage: false
} & Foo
]
type TotalData = Condition1 | Condition2

const data: TotalData = {

canManage: false,
extraProp: 5 // now typescript complains that this property shouldn't be here because canManage is false
}

当尝试基于嵌套对象内的属性设置联合时,我该如何解决此问题?


type Foo = { abilities: { canManage: boolean }; extraProp?: never }符合你的需求吗?只有在特定情况下才禁止使用额外的属性,而这似乎不是其中之一。如果这适合您,我会写出一个答案;如果不行,请解释一下哪个地方不符合您的用例。祝好运! - jcalz
那个解决方案暂时可能有效,但它有点不太正规,由于这是 TypeScript 的一个真实问题,你应该提交一个错误报告。 - Robert Moore
4.2 可能解决此问题:https://github.com/microsoft/TypeScript/pull/38839 - user2771609
不幸的是,不行 - jcalz
1个回答

12
编译器无法理解“嵌套判别联合”的概念。如果联合的成员共享一个常见的“鉴别”属性,则类型就是判别联合。鉴别属性通常是单例/文字类型,如 true"hello"123,甚至包括 nullundefined。但是,您不能使用另一个判别联合作为自身的鉴别器。如果可以这样做,那将很好,因为然后判别联合可以从嵌套属性中向上传播。有一个建议在microsoft/TypeScript#18758 提出允许这样做,但我没有看到任何进展。
目前,类型 TotalData 不是一个“判别式”联合。它只是一个联合。这意味着编译器不会尝试将类型为 TotalData 的值视为完全是 Condition1Condition2。因此,如果编写测试 data.abilities.canManage 并期望编译器理解其含义的代码,可能会遇到问题。
function hmm(x: TotalData) {
    if (x.abilities.canManage) {
        x.extraProp.toFixed(); // error!
    //  ~~~~~~~~~~~ <--- possibly undefined?!
    } 
}

如果您想要这样做,您可能需要编写用户定义的类型保护函数

function isCondition1(x: TotalData): x is Condition1 {
    return x.abilities.canManage;
}

function hmm(x: TotalData) {
    if (isCondition1(x)) {
        x.extraProp.toFixed(); // okay!
    } 
}

在这里遇到的具体问题,即 data 被视为有效的TotalData,与TypeScript中执行过多属性检查的方式有关。在TypeScript中,对象类型是“开放”的/“可扩展的”,而不是“关闭的” / “精确的”。您可以添加未在类型定义中提到的额外属性,而不违反类型。因此,编译器不能完全禁止超出属性;相反,它使用启发式方法尝试确定这些属性是错误还是有意的。主要使用的规则是:如果您正在创建全新的对象文字,并且它的任何属性未在使用它的类型中提到,则会发生错误。否则就不会。
如果TotalData是一个判别式联合,则在 data.abilities.canManage 导致编译器将 data TotalData 缩小到不包含extraProp Condition2 时,您会得到所期望的错误。但事实并非如此,因此 data 仍然是TotalData,其中确实提到了 extraProp
microsoft/TypeScript#20863中提出了当非判别式联合时应更严格地执行过多属性检查的建议。我基本上同意;混合匹配来自不同联合成员的属性似乎不是常见用例,因此警告可能会有所帮助。但是,这又是一个长期存在的问题,并且我没有看到任何进展。
你可以做的一件事是更明确地防范您希望避免的过度属性。类型为{a: string}的值可以具有类型为stringb属性,但是类型为{a: string, b?:never} 的值不能。 因此,后者的类型将防止 b 类型的属性,而不依赖于编译器对超额属性检查的启发式方法。
在您的情况下:
type Foo = {
    abilities: {
        canManage: boolean
    };
    extraProp?: never
}

它将非常类似于您原始的 Foo 定义,但现在您会得到此错误:

const data: TotalData = { // error!
// -> ~~~~
// Type '{ abilities: { canManage: false; }; extraProp: number; }'
// is not assignable to type 'TotalData'.
    abilities: {
        canManage: false,
    },
    extraProp: 5
}

编译器无法将dataCondition1Condition2协调一致,因此会发出投诉。


好的,希望对您有所帮助;祝你好运!

代码的 Playground 链接


感谢您详细的回复!真的帮助我理解了正在发生的事情,我非常感激。 - Showner91
2
这个问题可能会在4.2版本中得到解决!https://github.com/microsoft/TypeScript/pull/38839 - user2771609
1
不幸的是,不行 - jcalz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接