Utilizing the spread operator in Typescript to combine multiple Maps into a fresh Map leads to an instance of a clear Object

Check out the code below:

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);

Could anyone explain why result is not populated with the three key-value pairs as expected? It seems like I am missing a fundamental TypeScript concept.

I made sure to use the built-in Map type and avoided importing anything external.

Edit

Appreciate the guidance on how to properly merge Maps. My concern lies in

  1. why there are no compile-time errors in the provided code?
  2. why the merging technique doesn't produce the desired output?

Answer №1

The method of merging you used is ineffective because spreading Maps does not duplicate anything

If we use object spread like {...m1, ...m2, ...m3}, it only duplicates the objects' own enumerable properties. This means that it copies only the properties directly belonging to the objects themselves, and not those inherited from their prototypes. Additionally, it only includes the enumerable properties of the objects, such as those generated through assignment or defined as public class fields, excluding non-enumerable properties like class methods or getters. When we examine a Map object using

Object.getOwnPropertyDescriptors()
and Object.getPrototypeOf(), we observe 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 
...
*/

As shown, there are no own enumerable properties in the Map object, thus spreading a Map object does not actually copy any properties:

const copy = {...map};
console.log(copy) // {}
displayProps(copy);
/*
toString: inherited non-enumerable
toLocaleString: inherited non-enumerable 
...
*/

This demonstrates why attempting to merge Maps using spread syntax fails during runtime.


TypeScript did not identify the error of assigning the result to Map due to its inability to differentiate between own/enumerable properties

Given that {...map} results in an empty object, why does TypeScript not raise an error when assigning it to a Map type?

const result: Map<string, number>
  = { ...map, ...map, ...map }; // no error

The issue lies in the fact that the current TypeScript type system lacks the capability to specify whether a property is own/inherited or enumerable/non-enumerable. There exists an ongoing request for this feature at microsoft/TypeScript#9726. Until such functionality becomes a part of TypeScript, the language remains unaware of whether a Map's properties are all own and enumerable. In theory, it would be possible to create an object that mimics a Map instance but with all own and enumerable properties, fooling TypeScript into considering it a valid operation:

// Example similar to a Map instance
const fakeMap: Map<string, number> = {
  get: () => 1, has: () => true, set: () => o,
  ...
}

const p: Map<string, number> = { ...fakeMap }
displayProps(p);
/* 
get: own enumerable
has: own enumerable
set: own enumerable
... 
*/

Due to the compiler's inability to distinguish these scenarios, spreading operations on Map instances appear acceptable to TypeScript.

For more details, refer to microsoft/TypeScript#33081, which addresses this specific issue of spreading Map instances and was closed as a duplicate of microsoft/TypeScript#9726.


In summary, copying Map instances through object spread is not feasible, and TypeScript does not detect this discrepancy.

Playground link to code

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

forEach was unable to determine the appropriate data types

define function a(param: number): number; define function b(param: string): string; define function c(param: boolean): boolean; type GeneralHooks<H extends (...args: any[]) => any> = [H, Parameters<H>] const obj = { a: [a, [1]] as Gene ...

A tutorial on how to customize the hover effect for TableHead Column Identifiers in MaterialUI by adjusting

I'm struggling to customize the appearance of child th elements using the TableHead component from MaterialUI. While I've been successful in modifying various properties, I'm facing difficulty in changing the hover color. Below is the snipp ...

Exploring Angular's filtering capabilities and the ngModelChange binding

Currently, I am in the process of working on a project for a hotel. Specifically, I have been focusing on developing a reservation screen where users can input information such as the hotel name, region name, check-in and check-out dates, along with the nu ...

Encountering problems with TypeScript in a Node application when using the package manager

I am facing an issue while trying to package my node app into an exe using "pkg". I work with TypeScript, and when I attempt to run pkg index.ts --debug, an error pops up. Node.js v18.5.0 Warning Failed to make bytecode node18-x64 for file /snapshot/ ...

Uploading multiple strings to an Amazon S3 bucket using Node.js by piping a string

Suppose I have a simple loop similar to the one shown below: for (const i=0; i<3; i++) { to(`This incrementer is ${i}`) } At the end of the loop, I expect my file to contain: This counter is 0 This counter is 1 This counter is 2 I at ...

Guide to converting a string into an undefined type

I've been working on parsing a csv file: let lines = csvData.split(/\r\n|\n/); let headers = lines[0].split(','); for (let i = 1; i < lines.length; i++) { let values = lines[i].split(','); ...

Creating Beautiful MDX Layouts with Chakra UI

Currently, I am attempting to customize markdown files using Chakra UI within a next.js application. To achieve this, I have crafted the subsequent MDXComponents.tsx file: import { chakra } from "@chakra-ui/react" const MDXComponents = { p: (p ...

Refreshing Form in Angular 2

When I remove a form control from my form, it causes the form to always be invalid. However, if I delete a character from another input field and then add the same character back in (to trigger a change event), the form becomes valid as expected. Is ther ...

Missing data: null or undefined?

When using vuex-module-decorators, is it better to set the default value of a data property to null or undefined? For example: export default class ComponentName extends Vue { post: BlogPost | null = null } or export default class ComponentName extends V ...

An error in typescript involving a "const" assertion and a string array

Currently, I am diving into the world of Typescript along with React. However, an error has emerged in my path that I can't seem to figure out. It's puzzling why this issue is occurring in the first place. Allow me to elaborate below. const color ...

Implementing conditional where clauses in Firestore queries with dynamic parameters

Consider this scenario: I have a dynamic filter list for my product list, and I want to send an HTTPS request to a cloud function based on the selected filters. However, when trying to set multiple conditional where clauses from that request... The multip ...

The type entity i20.CdkScrollableModule cannot be resolved to symbol in the nx workspace

After extensively researching online, I still haven't found a solution to this particular issue. I've attempted various troubleshooting steps, including deleting node_modules and package-lock.json, updating dependencies, and running nx migrate. ...

Can someone explain how to create a Function type in Typescript that enforces specific parameters?

Encountering an issue with combineReducers not being strict enough raises uncertainty about how to approach it: interface Action { type: any; } type Reducer<S> = (state: S, action: Action) => S; const reducer: Reducer<string> = (state: ...

What is the best practice for inserting typescript definitions and writing them when the object already has a pre-existing definition?

Apologies for this question, as I am struggling to find the necessary information due to my limited understanding of Typescript. I have integrated a jquery plugin called typeahead and added a global variable named bound on the window object for communicati ...

Stop Mat-chip from automatically inserting a row upon selection

I am working on preventing the automatic addition of a row by the mat-chip module after a single chip has been selected. Even though the max chip count is set to 1, the input remains enabled and adds a new row beneath it as if the user can still type more ...

Facing problem with implementing NgMoudleFactoryLoader for lazy loading in Angular 8

A situation arose where I needed to lazy load a popups module outside of the regular router lazy-loading. In order to achieve this, I made the following adjustments: angular.json "architect": { "build": { ... "options": { ... "lazyM ...

Injecting services and retrieving data in Angular applications

As a newcomer to Angular, I am trying to ensure that I am following best practices in my project. Here is the scenario: Employee service: responsible for all backend calls (getEmployees, getEmployee(id), saveEmployee(employee)) Employees components: displ ...

The disappearance of the "Event" Twitter Widget in the HTML inspector occurs when customized styles are applied

Currently, I am customizing the default Twitter widget that can be embedded on a website. While successfully injecting styles and making it work perfectly, I recently discovered that after injecting my styles, clicking on a Tweet no longer opens it in a ne ...

What could be causing the "buffer is not a function" error to occur?

As a beginner with observables, I'm currently working on creating an observable clickstream to avoid capturing the 2 click events that happen during a double-click. However, I keep encountering this error message:- Error: Unhandled Promise rejection ...

There are two modals present on the page, however only one of them is triggered for all actions in Angular 2

I'm encountering an issue with my page where I have set up two confirmation modals - one for resetting a form and another for deleting an item. Strangely, only the reset modal is being triggered for both actions and I can't figure out why. Could ...