Skip to content

Commit 3db6d45

Browse files
committed
feat(primordials): add Set composition ambient declarations + tests
Set methods are TC39 Stage 4 (proposal-set-methods), shipped in Node 22+, but TypeScript's lib.es2024.* doesn't surface them. Ambient- declare difference / intersection / isDisjointFrom / isSubsetOf / isSupersetOf / symmetricDifference / union on Set + the ReadonlySetLike interface they accept until lib catches up. Companion test/unit/primordials/map-set.test.mts covers the getOrInsert + getOrInsertComputed entries from d8d20c3 plus the new Set composition surface — 14 tests.
1 parent 4895ae9 commit 3db6d45

2 files changed

Lines changed: 172 additions & 3 deletions

File tree

src/primordials/map-set.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77

88
import { uncurryThis } from './uncurry'
99

10-
// TC39 Stage 3 `getOrInsert` proposal — Node 22.10+ ships these, but
11-
// TypeScript's lib.es2024.* still lacks them. Ambient-declare here until
12-
// the lib catches up.
10+
// Stage 3+ TC39 proposals that Node 22+ ships but TypeScript's
11+
// lib.es2024.* still lacks. Ambient-declare here until the lib catches
12+
// up. References:
13+
// - getOrInsert: https://github.com/tc39/proposal-upsert
14+
// - Set composition: https://github.com/tc39/proposal-set-methods
1315
declare global {
1416
interface Map<K, V> {
1517
getOrInsert(key: K, value: V): V
@@ -19,6 +21,20 @@ declare global {
1921
getOrInsert(key: K, value: V): V
2022
getOrInsertComputed(key: K, callbackfn: (key: K) => V): V
2123
}
24+
interface ReadonlySetLike<T> {
25+
has(value: T): boolean
26+
keys(): IterableIterator<T>
27+
readonly size: number
28+
}
29+
interface Set<T> {
30+
difference<U>(other: ReadonlySetLike<U>): Set<T>
31+
intersection<U>(other: ReadonlySetLike<U>): Set<T & U>
32+
isDisjointFrom(other: ReadonlySetLike<unknown>): boolean
33+
isSubsetOf(other: ReadonlySetLike<unknown>): boolean
34+
isSupersetOf(other: ReadonlySetLike<unknown>): boolean
35+
symmetricDifference<U>(other: ReadonlySetLike<U>): Set<T | U>
36+
union<U>(other: ReadonlySetLike<U>): Set<T | U>
37+
}
2238
}
2339

2440
// ─── Constructors ──────────────────────────────────────────────────────
@@ -47,10 +63,21 @@ export const MapPrototypeValues = uncurryThis(Map.prototype.values)
4763
export const SetPrototypeAdd = uncurryThis(Set.prototype.add)
4864
export const SetPrototypeClear = uncurryThis(Set.prototype.clear)
4965
export const SetPrototypeDelete = uncurryThis(Set.prototype.delete)
66+
export const SetPrototypeDifference = uncurryThis(Set.prototype.difference)
5067
export const SetPrototypeEntries = uncurryThis(Set.prototype.entries)
5168
export const SetPrototypeForEach = uncurryThis(Set.prototype.forEach)
5269
export const SetPrototypeHas = uncurryThis(Set.prototype.has)
70+
export const SetPrototypeIntersection = uncurryThis(Set.prototype.intersection)
71+
export const SetPrototypeIsDisjointFrom = uncurryThis(
72+
Set.prototype.isDisjointFrom,
73+
)
74+
export const SetPrototypeIsSubsetOf = uncurryThis(Set.prototype.isSubsetOf)
75+
export const SetPrototypeIsSupersetOf = uncurryThis(Set.prototype.isSupersetOf)
5376
export const SetPrototypeKeys = uncurryThis(Set.prototype.keys)
77+
export const SetPrototypeSymmetricDifference = uncurryThis(
78+
Set.prototype.symmetricDifference,
79+
)
80+
export const SetPrototypeUnion = uncurryThis(Set.prototype.union)
5481
export const SetPrototypeValues = uncurryThis(Set.prototype.values)
5582

5683
// ─── WeakMap (prototype) ───────────────────────────────────────────────
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* @file Unit tests for src/primordials/map-set — Map/Set/WeakMap/WeakSet
3+
* prototype primordials. Focuses on the newer methods (Stage 3+ proposals
4+
* Node ships ahead of the TS lib) where regressions are most likely.
5+
*/
6+
7+
import { describe, expect, it } from 'vitest'
8+
9+
import {
10+
MapPrototypeGetOrInsert,
11+
MapPrototypeGetOrInsertComputed,
12+
SetPrototypeDifference,
13+
SetPrototypeIntersection,
14+
SetPrototypeIsDisjointFrom,
15+
SetPrototypeIsSubsetOf,
16+
SetPrototypeIsSupersetOf,
17+
SetPrototypeSymmetricDifference,
18+
SetPrototypeUnion,
19+
WeakMapPrototypeGetOrInsert,
20+
WeakMapPrototypeGetOrInsertComputed,
21+
} from '../../../src/primordials/map-set'
22+
23+
describe('Map.prototype.getOrInsert primordials', () => {
24+
it('MapPrototypeGetOrInsert inserts when key is missing', () => {
25+
const m = new Map<string, number>()
26+
expect(MapPrototypeGetOrInsert(m, 'a', 1)).toBe(1)
27+
expect(m.get('a')).toBe(1)
28+
})
29+
30+
it('MapPrototypeGetOrInsert returns the existing value without overwriting', () => {
31+
const m = new Map<string, number>([['a', 7]])
32+
expect(MapPrototypeGetOrInsert(m, 'a', 99)).toBe(7)
33+
expect(m.get('a')).toBe(7)
34+
})
35+
36+
it('MapPrototypeGetOrInsertComputed only invokes the callback when missing', () => {
37+
const m = new Map<string, number>([['a', 7]])
38+
let calls = 0
39+
expect(
40+
MapPrototypeGetOrInsertComputed(m, 'a', () => {
41+
calls += 1
42+
return 99
43+
}),
44+
).toBe(7)
45+
expect(calls).toBe(0)
46+
47+
expect(
48+
MapPrototypeGetOrInsertComputed(m, 'b', () => {
49+
calls += 1
50+
return 42
51+
}),
52+
).toBe(42)
53+
expect(calls).toBe(1)
54+
expect(m.get('b')).toBe(42)
55+
})
56+
57+
it('MapPrototypeGetOrInsertComputed passes the key to the callback', () => {
58+
const m = new Map<string, string>()
59+
const result = MapPrototypeGetOrInsertComputed(m, 'foo', k =>
60+
k.toUpperCase(),
61+
)
62+
expect(result).toBe('FOO')
63+
expect(m.get('foo')).toBe('FOO')
64+
})
65+
})
66+
67+
describe('WeakMap.prototype.getOrInsert primordials', () => {
68+
it('WeakMapPrototypeGetOrInsert inserts when key is missing', () => {
69+
const m = new WeakMap<object, number>()
70+
const key = {}
71+
expect(WeakMapPrototypeGetOrInsert(m, key, 1)).toBe(1)
72+
expect(m.get(key)).toBe(1)
73+
})
74+
75+
it('WeakMapPrototypeGetOrInsert returns existing value', () => {
76+
const m = new WeakMap<object, number>()
77+
const key = {}
78+
m.set(key, 7)
79+
expect(WeakMapPrototypeGetOrInsert(m, key, 99)).toBe(7)
80+
expect(m.get(key)).toBe(7)
81+
})
82+
83+
it('WeakMapPrototypeGetOrInsertComputed only invokes callback when missing', () => {
84+
const m = new WeakMap<object, number>()
85+
const key = {}
86+
let calls = 0
87+
expect(
88+
WeakMapPrototypeGetOrInsertComputed(m, key, () => {
89+
calls += 1
90+
return 42
91+
}),
92+
).toBe(42)
93+
expect(calls).toBe(1)
94+
expect(
95+
WeakMapPrototypeGetOrInsertComputed(m, key, () => {
96+
calls += 1
97+
return 99
98+
}),
99+
).toBe(42)
100+
expect(calls).toBe(1)
101+
})
102+
})
103+
104+
describe('Set composition primordials', () => {
105+
const a = new Set([1, 2, 3])
106+
const b = new Set([2, 3, 4])
107+
108+
it('SetPrototypeDifference returns elements in a but not b', () => {
109+
const result = SetPrototypeDifference(a, b)
110+
expect([...result].sort()).toEqual([1])
111+
})
112+
113+
it('SetPrototypeIntersection returns elements in both', () => {
114+
const result = SetPrototypeIntersection(a, b)
115+
expect([...result].sort()).toEqual([2, 3])
116+
})
117+
118+
it('SetPrototypeUnion returns elements in either', () => {
119+
const result = SetPrototypeUnion(a, b)
120+
expect([...result].sort()).toEqual([1, 2, 3, 4])
121+
})
122+
123+
it('SetPrototypeSymmetricDifference returns elements in either but not both', () => {
124+
const result = SetPrototypeSymmetricDifference(a, b)
125+
expect([...result].sort()).toEqual([1, 4])
126+
})
127+
128+
it('SetPrototypeIsSubsetOf returns true when all elements appear in the other', () => {
129+
expect(SetPrototypeIsSubsetOf(new Set([2, 3]), a)).toBe(true)
130+
expect(SetPrototypeIsSubsetOf(a, b)).toBe(false)
131+
})
132+
133+
it('SetPrototypeIsSupersetOf returns true when all of the other appear in this', () => {
134+
expect(SetPrototypeIsSupersetOf(a, new Set([2, 3]))).toBe(true)
135+
expect(SetPrototypeIsSupersetOf(a, b)).toBe(false)
136+
})
137+
138+
it('SetPrototypeIsDisjointFrom returns true when no elements are shared', () => {
139+
expect(SetPrototypeIsDisjointFrom(a, new Set([10, 20]))).toBe(true)
140+
expect(SetPrototypeIsDisjointFrom(a, b)).toBe(false)
141+
})
142+
})

0 commit comments

Comments
 (0)