TypeScript中联合类型和对象类型的不一致行为

4

让我们看下面的代码片段:

interface A {
  isEmpty(x: any[] | string | object): boolean;
  isEmptyObject(x: object): boolean;
}

const a: A = <A>{};

a.isEmptyObject({});
a.isEmptyObject({a: 1});

a.isEmpty([]);
a.isEmpty('');
a.isEmpty({});
a.isEmpty({a: 1});

当我尝试编译它时,它会崩溃并出现以下错误信息:
a.ts(14,12): error TS2345: Argument of type '{ a: number; }' is not assignable to parameter of type 'string | object | any[]'.
  Object literal may only specify known properties, and 'a' does not exist in type 'string | object | any[]'.

当联合类型(string | object | any[])包含类型object时,为什么它可以工作(使用{a:1}isEmptyObject调用是可以的)?
主要问题:这是TypeScript的bug吗?还是应该如何编写输入可能是任何值或字符串或对象的数组(但不是任何原始类型,我不想要anyObject)?
使用TypeScript 2.3.4进行测试。

@adiga 我不明白这与问题有何关系。我没有访问联合类型参数 x 中的任何字段。 - monnef
重新排列联合类型会改变什么吗?我不知道为什么会发生这种情况,但现在我很好奇。 - Carcigenicate
@monnef 抱歉,我意识到了并删除了它。 - adiga
@Carcigenicate 我在 playground 中尝试了一下,但顺序似乎没有任何影响 :-/. - monnef
@monnef 哎呀,我只用了一个月的TS,所以我只是在瞎猜。看起来像是个bug,但这不会是第一件看起来像是bug,实际上却是TS的某些边缘情况的事情。 - Carcigenicate
3个回答

4
这不是一个 bug,而是支持使用参数对象的 API 的特性。以 jQuery 的 ajax 函数为例,它将一个对象作为参数。
interface JQueryAjaxParam {
    url: string;
    data?: any;
}

...

$.ajax({
    url: "",
    datta: "" // notice the typo here
});

在这种情况下,您确实希望出现编译错误,即使{url:"",datta:""}实现了param接口。为了克服这个问题,他们添加了对象文字作为直接参数的多余属性检查。这是件好事,可以提供更多类型检查器的信心,并且只会在人们尝试进行类似快速测试的时候导致错误。您必须记住这个规则,这是额外安全性的代价。

更新:TypeScript 2.4还引入了弱类型

那么有这个签名的函数:

function isEmptyObject(x : object) : void;

这里的意图很清晰,该函数旨在接受任何具有任何键的对象。仅接受空对象的函数几乎没有用处。因此,他们添加了一个例外:当参数恰好是object类型时,先前的检查将被省略。
所以要做什么?什么都不用做。在实际使用中,您的函数将进行类型检查。您可以尝试一下:
interface A {
  isEmpty(x: any[] | string | object): boolean;
  isEmptyObject(x: object): boolean;
}

const a: A = <A>{};
const param = {a: 1};

a.isEmptyObject({});
a.isEmptyObject(param);

a.isEmpty([]);
a.isEmpty('');
a.isEmpty({});
a.isEmpty(param);

更新

您的示例是一个实际用例,您希望它甚至可以与文字一起使用。在Nitzan之后,我也建议为此使用函数重载

interface A {
    isEmpty(x: any[]): boolean;
    isEmpty(x: string): boolean;
    isEmpty(x: object): boolean;
    isEmptyObject(x: object): boolean;
}

const a: A = <A>{};
a.isEmptyObject({b: 6});
a.isEmptyObject({a: 1});

a.isEmpty([]);
a.isEmpty('');
a.isEmpty({});
a.isEmpty({a: 1});

这是一个真实的应用场景 - 我正在包装一个现有的库函数以修复类型。我需要任何类型、字符串或非原始类型(=object)的数组。当它被定义为非原始类型时,我看不到为什么object会意味着"只有空对象"。当类型检查是不可传递的时,这对我来说似乎不是一个好事情。._. - monnef
这与 suppressExcessPropertyErrors 编译器选项有关吗? - cartant
@monnef 我明白你的意思。让我试验一下并更新我的答案。 - Tamas Hegedus
@monnef 是的。我想我们刚刚目睹了一些实现细节泄漏到使用中。也许值得在 Github 上开启一个讨论线程。 - Tamas Hegedus
1
看起来这实际上是 TypeScript 编译器的一个 bug - https://github.com/DefinitelyTyped/DefinitelyTyped/issues/17007。我松了一口气,因为这真的是很奇怪的行为。 - monnef
显示剩余2条评论

1

Typescript在以下情况下无法编译:

对象文字只能指定已知属性,而'a'不存在于类型'string | object | any[]'中。

基本上,{ a: 1 }不能转换为类型object

你可以这样做:

interface A {
    isEmpty(x: any[] | string | { [index: string]: any }): boolean;
    isEmptyObject(x: { [index: string]: any }): boolean;
}

编辑:再次查看文档后,可能可以解释为:

联合类型在这里可能有点棘手,但只需要一点直觉就能习惯。如果一个值的类型为A | B,则我们只知道它具有A和B都具有的成员。在这个例子中,Bird有一个名为fly的成员。我们无法确定一个类型为Bird | Fish的变量是否具有fly方法。如果变量在运行时确实是Fish,则调用pet.fly()将失败。


1
这不可能是真的 - {a: 1} 明显可以转换为 object,因为 a.isEmptyObject({a: 1}); 可以正常工作而没有任何警告或错误。 - monnef
1
请使用缩进而不是反引号来格式化较大的代码块。反引号会使任何较大的代码块难以阅读。 - Carcigenicate
看了文档后,我认为这是由于联合类型组合了“any[]和object”的方式。请查看我的编辑。 - Sean Sobey
@SeanSobey,文档中的引用似乎涉及到使用联合类型的变量/参数 - 我没有在任何地方使用 x,它只是一个接口(用于展示它不通过类型检查)。 - monnef

1

您可以通过添加另一个签名轻松解决此问题:

interface A {
    isEmpty(x: any[]): boolean;
    isEmpty(x: string | object): boolean;
    isEmptyObject(x: object): boolean;
}

a.isEmpty({ a: 1 }); // fine now

(在playground中的代码)


编辑

我认为这不是一个bug,如果有什么问题的话,那就是错误信息不够明确。

我认为问题出在你可以在联合中使用object和任何原始类型而不会出现问题(在你的情况下),但是一旦引入非原始类型,你就会得到错误,例如:

isEmpty(x: string | object | Map<string, string>): boolean;

会导致相同的错误。
object(即任何非原始类型)与另一个非原始类型的联合可能会导致无法匹配的类型,从而使编译器混淆。

如果您愿意,请在此情况下打开新问题,如果您这样做,请在此处分享链接,我想看看他们说什么。


但是为什么这是必要的? - Carcigenicate
所以这是 TypeScript 的 bug 吗?还是为什么我必须绕过这些障碍呢? - monnef
@Carcigenicate和OP,请检查我的修订答案。 - Nitzan Tomer

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