diff --git a/src/util/map.js b/src/util/map.js index faf318c..b4646d0 100644 --- a/src/util/map.js +++ b/src/util/map.js @@ -122,3 +122,27 @@ export function mapEntries( } return result; } + +/** + * Merge maps without mutating the arguments. + * + * Merges multiple maps, returning a new map which has every key from + * the source maps, with their corresponding values. None of the inputs + * are mutated. In the event that multiple maps have the same key, an + * error will be thrown. + */ +export function merge( + maps: $ReadOnlyArray, $Subtype>> +): Map { + const result = new Map(); + let updates = 0; + for (const map of maps) { + for (const [key, value] of map.entries()) { + result.set(key, value); + if (result.size !== ++updates) { + throw new Error(`Maps have duplicate key: ${String(key)}`); + } + } + } + return result; +} diff --git a/src/util/map.test.js b/src/util/map.test.js index 05dde90..577fd2d 100644 --- a/src/util/map.test.js +++ b/src/util/map.test.js @@ -245,4 +245,53 @@ describe("util/map", () => { expect(output).toEqual(new Map().set(11, "wat").set(12, "wat")); }); }); + describe("merge", () => { + it("combines two simple maps", () => { + const a = new Map().set("a", 1); + const b = new Map().set("b", 2); + const c = new Map().set("c", 3); + expect(MapUtil.merge([a, b, c])).toEqual( + new Map() + .set("a", 1) + .set("b", 2) + .set("c", 3) + ); + }); + it("treats empty map as an identity", () => { + const m = new Map().set("a", 11).set("b", 22); + expect(MapUtil.merge([new Map(), m, new Map()])).toEqual(m); + }); + it("errors if there are any duplicate keys", () => { + const a = new Map().set("a", null); + expect(() => MapUtil.merge([a, a])).toThrowError("duplicate key"); + }); + it("handles null and undefined appropriately", () => { + const a = new Map().set(undefined, undefined); + const b = new Map().set(null, null); + expect(MapUtil.merge([a, b])).toEqual( + new Map().set(undefined, undefined).set(null, null) + ); + }); + it("merge works on empty list", () => { + expect(MapUtil.merge([])).toEqual(new Map()); + }); + it("allows upcasting the type parameters", () => { + const numberMap: Map = new Map().set(1, 2); + const stringMap: Map = new Map().set("one", "two"); + type NS = number | string; + const _unused_polyMap: Map = MapUtil.merge([ + numberMap, + stringMap, + ]); + }); + it("produces expected type errors", () => { + const numberMap: Map = new Map().set(1, 2); + const stringMap: Map = new Map().set("one", "two"); + // $ExpectFlowError + const _unused_badMap: Map = MapUtil.merge([ + numberMap, + stringMap, + ]); + }); + }); });