I recently started using typescript and I've run into a confusing little part, where I don't quite get why typescript behaves like it does. (Involving typechecking a parameter that could either be an array or an object)
I first tried checking as the upper example shows, but it didn't work (Typescript didn't like me calling .map
Error Message: This expression is not callable. Not all constituents of type 'string | ((callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any) => U[])' are callable. Type 'string' has no call signatures.ts(2349)
I figured it had something to do with calling map on objects so I replaced the condition with Array.isArray() - and suddenly typescript was happy. - But why wasn't it before? - because in my mind objects wouldn't be able to pass regardless.
interface Object {
[key: string]: string
}
function myFunction(a: string[] | Object) {
if(a.length) {
a.map(x => x)
}
}
if I change the condition from a.length
to Array.isArray(a)
typescript realizes that objects won't pass this check - but why didn't it before? (since a.length
will return undefined if called on an object - so the check would always fail if a was an object)
So I guess my question is: Why does typescript only accept Array.isArray()
as a valid check in this case? - Or is there something else going on I am not aware of?
interface Object {
[key: string]: string
}
function myFunction(a: string[] | Object) {
if(Array.isArray(a)) {
a.map(x => x)
}
}
CodePudding user response:
Consider the following code:
interface Object {
[key: string]: string
}
function myFunction(a: string[] | Object) {
if(a.length) {
console.log(`I think this is an array: ${a}; it's type is ${typeof a} and it ${Array.isArray(a) ? "is" : "isn't"} an array`);
} else {
console.log(`I think this is not an array: ${a}; it's type is ${typeof a} and it ${Array.isArray(a) ? "is" : "isn't"} an array`);
}
}
myFunction(["a"]);
myFunction({ length: "hello" });
Any object can have the length
property, not just arrays. If you want to go down the route of checking for a property, you could just check those you want to use:
interface Object {
[key: string]: string
}
function myFunction(a: string[] | Object) {
if(a.hasOwnProperty("map") && typeof a.map === "function") {
const b = a.map(x => x);
}
}
CodePudding user response:
Your problem is an incorrect assumption:
since a.length will return undefined if called on an object - so the check would always fail if a was an object
Because you defined Object with [key: string]: string
, it could very well have a length
property, though it would be a string.
You can examine the following proof in Typescript Playground:
interface Object {
[key: string]: string
}
function getLength(o: Object) {
return o.length
}
const obj: Object = {length: 'long'}
const x = getLength(obj) // x is of type string
const objTwo: Object = {}
const x2 = getLength(obj) // x2 is still of type string
The only failure I see in Typescript's type inference is that the return type of getLength
should be string | undefined
. I'm not sure why Typescript fails to figure this out.
Nonetheless, the fact that an Object instance could have a length property means your logic is incorrect.