Reputation: 4053
Let's have a following snippet:
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});
When I try to compile it, it crashes with this:
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[]'.
How is this possible, when the union type (string | object | any[]
) contains type object
for which it works (the isEmptyObject
call with {a: 1}
is fine)?
Main question: Is this a TypeScript bug, or how one should write that input may be an array of any value or string or object (but not any primitive, I don't want any
or Object
)?
Tested with TypeScript 2.3.4.
Upvotes: 4
Views: 856
Reputation: 29906
This is not a bug but a feature to support API-s which utilizes argument objects. Take for example jQuery-s ajax function. It takes an object as an argument.
interface JQueryAjaxParam {
url: string;
data?: any;
}
...
$.ajax({
url: "",
datta: "" // notice the typo here
});
You do want a compilation error in this case, even though {url:"",datta:""}
implements the param interface. To overcome this they added excess property checks for object literals as direct parameters. This is a good thing, provides more confidence in the typechecker, and only causes false errors when people try to conduct quick tests like yours. You have to keep this rule in mind, this is the cost of additional safety.
UPDATE: TypeScript 2.4 additionally introduces Weak types
So then there are functions with this signature:
function isEmptyObject(x : object) : void;
Here, the intention is clear, this function aims to accept any object with any keys. A function accepting only empty object has little use. So they added an exception: when the parameter is of the type object
exactly, then the previous check is omitted.
So what to do? Nothing. In real uses your function will typecheck just fine. You can try it:
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);
UPDATE
Your example is an actual usecase, you want it to work even with literals. After Nitzan, I too suggest to use function overloading for this.
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});
Upvotes: 4
Reputation: 972
Typescript is failing the compilation on the following condition:
Object literal may only specify known properties, and 'a' does not exist in type 'string | object | any[]'.
Basically { a: 1 }
is not convertible to type object
.
You could do it this way:
interface A {
isEmpty(x: any[] | string | { [index: string]: any }): boolean;
isEmptyObject(x: { [index: string]: any }): boolean;
}
EDIT: after another look at the docs it is possibly explained by:
Union types can be a bit tricky here, but it just takes a bit of intuition to get used to. If a value has the type A | B, we only know for certain that it has members that both A and B have. In this example, Bird has a member named fly. We can’t be sure whether a variable typed as Bird | Fish has a fly method. If the variable is really a Fish at runtime, then calling pet.fly() will fail.
Upvotes: 1
Reputation: 164129
You can easily go around this problem by adding another signature:
interface A {
isEmpty(x: any[]): boolean;
isEmpty(x: string | object): boolean;
isEmptyObject(x: object): boolean;
}
a.isEmpty({ a: 1 }); // fine now
I don't think that it's a bug, if anything is a bug it's the error message which isn't very helpful.
What I think that happens is that you can use object
in a union with any primitive types without a problem (in your case), but once you introduce a non-primitive type then you'll get the error, for example:
isEmpty(x: string | object | Map<string, string>): boolean;
Will result in the same error.
The union of object
(which is anything that isn't primitive) and another non-primitive type probably results in a type that cannot be matched and the compiler gets confused.
You are welcome to open a new issue on this case, if you do please share the link here, I'd like to see what they say.
Upvotes: 1