Issue
In the following sample code:
let m1 = new Map<string, PolicyDocument>([
[
"key1",
new PolicyDocument({
statements: [
new PolicyStatement({
actions: ["sqs:PublishMessage"],
effect: Effect.ALLOW,
resources: ["aa:bb:cc"]
})
]
})
]
]);
let m2 = new Map<string, PolicyDocument>([
[
"key2",
new PolicyDocument({
statements: [
new PolicyStatement({
actions: ["sqs:GetMessage"],
effect: Effect.ALLOW,
resources: ["aa:bb:cc"]
})
]
})
]
]);
let m3 = new Map<string, PolicyDocument>([
[
"key3",
new PolicyDocument({
statements: [
new PolicyStatement({
actions: ["sqs:DeleteMessage"],
effect: Effect.ALLOW,
resources: ["aa:bb:cc"]
})
]
})
]
]);
const result: Map<string, PolicyDocument> = {
...m1,
...m2,
...m3
};
console.log(result);
Can someone help me understand why result is empty Object instead of having the three key,value pairs?
I'm overlooking some concept in typescript.
I use the native Map type provided without using any imports.
Edit
Thanks for sharing the correct way to merge Maps, that was helpful. My question is about
- why above code doesn't throw compile-time error?
- why above merge approach doesn't work?
Solution
Your merge approach doesn't work because spreading Maps doesn't copy anything
Object spread of the form {...m1, ...m2, ...m3} copies objects' own enumerable properties. It only copies objects' own properties; it does not copy properties that objects inherit through their prototypes. It only copies objects' enumerable properties, such as those created via assignment or as public class fields; it does not include non-enumerable properties like class methods or getters. If we inspect a Map object via Object.getOwnPropertyDescriptors() and Object.getPrototypeOf(), we see the following:
const map = new Map([["a", 1], ["b", 2]]);
console.log(map) // Map (2) {"a"=>1, "b"=>2}
displayProps(map);
/*
get: inherited non-enumerable
has: inherited non-enumerable
set: inherited non-enumerable
delete: inherited non-enumerable
keys: inherited non-enumerable
values: inherited non-enumerable
clear: inherited non-enumerable
forEach: inherited non-enumerable
entries: inherited non-enumerable
size: inherited non-enumerable
constructor: inherited non-enumerable
toString: inherited non-enumerable
toLocaleString: inherited non-enumerable
valueOf: inherited non-enumerable
hasOwnProperty: inherited non-enumerable
isPrototypeOf: inherited non-enumerable
propertyIsEnumerable: inherited non-enumerable
__defineGetter__: inherited non-enumerable
__defineSetter__: inherited non-enumerable
__lookupGetter__: inherited non-enumerable
__lookupSetter__: inherited non-enumerable
*/
Look at that; it has no own enumerable properties (indeed none of its properties are either own or enumerable), so spreading a Map object doesn't copy anything at all:
const copy = {...map};
console.log(copy) // {}
displayProps(copy);
/*
toString: inherited non-enumerable
toLocaleString: inherited non-enumerable
valueOf: inherited non-enumerable
hasOwnProperty: inherited non-enumerable
isPrototypeOf: inherited non-enumerable
propertyIsEnumerable: inherited non-enumerable
__defineGetter__: inherited non-enumerable
__defineSetter__: inherited non-enumerable
__lookupGetter__: inherited non-enumerable
__lookupSetter__: inherited non-enumerable
constructor: inherited non-enumerable
*/
And so merging Maps via spread doesn't work at runtime.
TypeScript didn't catch the mistake of assigning the result to Map because it can't really track whether properties are own or enumerable
So if {...map} is equivalent to {}, why doesn't TypeScript complain when you assign it to a Map type?
const result: Map<string, number>
= { ...map, ...map, ...map }; // no error
The problem is that the TypeScript type system currently has no way to express that a property is own/inherited or enumerable/non-enumerable. There is a longstanding open issue at microsoft/TypeScript#9726 requesting some way to specify this information. Until and unless something like this is implemented, it's not part of the language. So for all TypeScript knows, a Map's properties are all own and enumerable... and indeed it would be possible to write your own object which looks to TypeScript exactly like a Map instance except that its properties are all own and enumerable:"
const e: [string, number] = ["", 0]
const o: Map<string, number> = {
get: () => 1, has: () => true, set: () => o,
delete: () => true, keys: () => [""][Symbol.iterator](),
values: () => [0][Symbol.iterator](), clear: () => { },
forEach: () => { }, entries: () => [e][Symbol.iterator](),
size: 2, [Symbol.iterator]: () => [e][Symbol.iterator](),
[Symbol.toStringTag]: ""
}
const p: Map<string, number> = { ...o }
displayProps(p);
/* get: own enumerable
has: own enumerable
set: own enumerable
delete: own enumerable
keys: own enumerable
values: own enumerable
clear: own enumerable
forEach: own enumerable
entries: own enumerable
size: own enumerable */
Since the compiler can't tell the difference between these cases, we run into this limitation where spreading Map instances looks like a valid operation to TypeScript.
See microsoft/TypeScript#33081 which was specifically raised about this exact issue with spreading Map instances. It was closed as a duplicate of microsoft/TypeScript#9726.
There you go; we're kind of stuck. Map instances cannot be copied via object spread, and TypeScript isn't smart enough to catch it.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.