-
Notifications
You must be signed in to change notification settings - Fork 12
Bug in arrays map
return type
#35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
map
return type
Wow. Researching this issue opened up a big can of worms. And after studying what the ECMAScript specification says about My investigation began with the current type definition of map<U, This = undefined>(
callbackfn: (this: This, value: T, index: number, array: this) => U,
thisArg?: This
): { [K in keyof this]: U }; The culprit in this type definition is the return type But, what piqued my interest was why did the return type depend upon class StringArray extends Array<string> {
public constructor(strings: string[]) {
const { length } = strings;
super(length);
let index = 0;
while (index < length) {
this[index] = strings[index];
index = index + 1;
}
}
}
const strings = new StringArray(["hello", "world"]);
const numbers = strings.map((string) => string.length);
console.assert(numbers instanceof StringArray); // assertion passes This was surprising for me because I expected the result of Why is this a terrible design? First, consider that in our above example This can lead to subtle bugs, like when you map over an empty const noStrings = new StringArray([]);
const noNumbers = noStrings.map((string) => string.length);
console.log(noNumbers); // Expected to see `[]` but we see `[undefined]` instead So, what's the takeaway? Don't create sub classes of Coming back, what should the return type of Of course, this is also unsafe because the result is actually a In conclusion, just avoid all these problems by never creating a sub class of |
Yes, extending an array is a known security vulnerability that allows arbitrary code execution. Although, if you're aware of it, it can be fixed by overriding the To be clear I am not extending any arrays, this was just to show the example of the resulting type. I probably could've been more explicit about it, sorry, didn't have much time. As shown in my code sandbox playground I've encountered this issue bu implementing a custom string template tag function. Those functions receive a interface TemplateStringsArray extends ReadonlyArray<string> {
readonly raw: readonly string[];
} I see now what was your goal with overriding the return of the map<U, This = undefined>(
callbackfn: (this: This, value: T, index: number, array: this) => U,
thisArg?: This,
): this & U[]; Here's a Playground link with your type where it causes the conflict. And here is how this type fixes that conflict: Playground link However! if you run the code in the latest example you will see that there is another problem. That the mapped array does not have a Sadly there is no easy way to distinguish that, and although So, in the end, I would agree with you @aaditmshah and put the return type of those Array methods to be just |
It's my fault that I made this change in #12: The reason behind this change is to deal with mapping of TypeScript tuples, which is discussed in microsoft/TypeScript#29841: let point: [number, number] = [4, 2];
const distance = Math.hypot(...point);
point = point.map(coord => coord / distance); In order to solve this, I played around with the TypeScript repo and tried numerous possibilities in my old days, but none of the solutions seem feasible. The PR still exists there anyway: |
Thank you all for the discussion! I'm going to change the return type to just |
The reason the solution proposed in my old PR wasn't feasible was merely due to the difficulty of introducing breaking changes in TypeScript. |
Here's the minimal repro that I managed to extract: type IsNumConst<T> = [T] extends [number] ? ([number] extends [T] ? false : true) : false;
type LEN<T extends readonly any[]> = T extends { length: infer L } ? L : never;
type TupleOfSize<T, N extends number, _REST extends any[] = []> =
IsNumConst<N> extends true
? _REST extends { length: N }
? _REST
: TupleOfSize<T, N, [..._REST, T]>
: T[];
type MappedArray<T extends readonly any[], U> = TupleOfSize<U, LEN<T>>;
declare global {
interface ReadonlyArray<T> {
map<U>(cb: (item: T) => U): MappedArray<this, U>;
}
}
let three_numbers = [1, 2, 3] as const
let three_strings = three_numbers.map(v => String(v)); Not very minimal, but works. It uses recursion so it will obviously break at some point, but a tuple has to be very huge, I think about 1000 elements. |
@n1kk FYI here is an implementation that builds a tuple in O(log(n)): microsoft/TypeScript#26223 (comment) |
@graphemecluster Awesome, thanks! |
Hi, I checked if this works, and it indeed seemed to fix the issue without degrading the tuple type support. I'm going this way. Thank you 🙂 |
The return type for the
map
method inArray
andReadonlyArray
seems to be causing some trouble.In case when it is an instance of the class that extends the
Array
it will change all the keys of the class to the mapped value type, including methods and public fields.CodeSandbox playground link
The text was updated successfully, but these errors were encountered: