I'm using React and TypeScript. When I'm iterating through an array using map(), it seems the types are not always checked. In the following example I pass a string "more" into the eachItem function, where number is expected. But no error is thrown:
function eachItem(val: number, i: number) {
return Number(val) / 2;
}
const arr = [4, "more", 6];
arr.map(eachItem);
I think I understand why this happens, but my question is how I can make strict typing on a map function, so that I get this error when passing in the string:
Argument of type 'string' is not assignable to parameter of type 'number'
Why no error is thrown is because I'm giving a callback to the map(). This map() expects a callback of this type:
'(value: string | number, index: number, array: (string | number)[]) => Element'
and I'm giving a callback of this type:
'(item: number, i: number) => Element'
So instead of checking whether 'number' includes 'string | number', TypeScript checks whether 'string | number' includes 'number' and finds it true. My logic says we need an error, since I'm passing "more" where only numbers are permitted. TypeScript's logic says no error, I'm passing a function that allows numbers where functions that allow strings and numbers are permitted. It doesn't seem to be a big issue, but when your type is a union, you get errors when you're not supposed to. This is an example, and the resulted error:
interface IMoney {
cash: number;
}
interface ICard {
cardtype: string;
cardnumber: string;
}
type IMix = IMoney | ICard;
const menu = [
{cash: 566},
{cardtype: "credit", cardnumber: "111111111111111111"}
];
menu.map((item: IMix, i: number) => (item.cash || item.cardtype));
Property 'cash' does not exist on type 'IMix'.
Property 'cash' does not exist on type 'ICard'.
I know, now I have to do the following, but then I can't express in my code that cash and cardtype exclude each other:
interface IMix {
cash?: number;
cardtype?: string;
cardnumber?: string;
}
Typescript is right about the error in your first example. Consider if we force the compiler to accept a modified function:
function eachItem(val: number, i: number) {
return val.toExponential(3) // only numbers will have toExponential
}
const arr = [4, "more", 6];
// eachItem will get called with numbers and string. the strings will cause a runtime error
// runtime error val.toExponential is not a function
arr.map(eachItem as any);
So the reason typescript does not allow this behavior is that it can't be proved to be correct for any function with the signature (val: number, i: number) => string
but rather it depends on the specific implementation of the function.
Now in your second example, the function could access the fields and it would not get a runtime error. The fields may be undefined but that does not matter much for this implementation. What we could do is create a type in which all possible fields in the union are optional, which is in fact another way of looking at the items in the array (just not the way typescript looks at it by default). Typescript will allow us to pass in function with such an argument, since it is safe, since all fields are optional the function would need to check before using them anyway.
interface IMoney {
cash: number;
}
interface ICard {
cardtype: string;
cardnumber: string;
}
type IMix = IMoney | ICard;
const menu = [
{ cash: 566 },
{ cardtype: "credit", cardnumber: "111111111111111111" }
];
type UnionToIntersection<U> = (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
type PartialUnion<T> = UnionToIntersection<T extends any ? Partial<T> : never>
menu.map((item: PartialUnion<IMix>, i: number) => (item.cash || item.cardtype));
See here for an explanation of UnionToIntersection
(and upvote the answer that makes this possible :) )
The T extends any ? Partial<T> : never
will take each member in the union (since conditional types distribute over unions) and make it Partial
meaning all fields are now optional. Then we use UnionToIntersection
to transform the union of partials into an intersection of partials. The result is a type with all the fields in each member of the union, but with all fields marked as partial.