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 Map
s 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 Map
s 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.