You have already received some explanation about Set's.
I'd like to stick with pure types.
First of all, TS does not play well with mutable values. It just does not track mutations.
Second, please keep in mind that my FIRST example is not super useful in practice. I just want to give you more overview how TS works.
My second example is much useful and I believe you can use it, however I have never extended any built- in classes in practice. I believe @T.J.Crowder can give some advise.
In order to force TS do some checking, we should create simple wrapper for our add logic.
const add = (set, value) => set.add(value)
Let's add some types:
type Value = [number, number];
In order to allow TS track our add operations, let's add some cache.
function add<S extends Set<Value> & { __cache?: any }, Vals extends Value>
(set: S, value: [...Vals]) {
return set.add(value)
}
Every time when add will be called, I will update { __cache?: any }.
{ __cache?: any } --> { __cache?: [] } --> { __cache?: [1,2] } --> { __cache?: [1,2] | [3,4] } etc ....
Here is our main logic:
type UpdateProp<T, Value> =
T extends { __cache?: any[] }
? Set<Value> & Partial<Record<'__cache', T['__cache'] | Value>>
: T & Partial<Record<'__cache', Value>>
type Check<T, Value> = T extends { __cache?: infer R } ? Value extends R ? 1 : 2 : 3
function add<
S extends Set<Value> | Set<Value> & { __cache?: Value },
Vals extends Value
>(set: S, value: [...Vals], ...flag: Check<S, Vals> extends 1 ? [never] : []): UpdateProp<S, Vals>
I expect that first argument will be always a Set<Value> (without cache) or Set<Value> & { __cache?: Value } with cache.
Vals extends Value infers our array of two numbers
UpdateProp<S, Vals> smth similar to
const updateProp=(obj,value)=>{
return {
...obj,
['__cache']:value
}
}
...flag: Check<S, Vals> extends 1 ? [never] : [] - small validation trick. Here, in my blog you can find full explanation
So, every time, when smbd call add(set, [2,3]) __cache property will be updated/unionized.
Please keep in mind, __cache is optional, so don't worry, TS will not allow smth like that:set.__cache.
Full code:
type Value = [number, number]
type UpdateProp<T, Value> =
T extends { __cache?: any[] }
? Set<Value> & Partial<Record<'__cache', T['__cache'] | Value>>
: T & Partial<Record<'__cache', Value>>
type Check<T, Value> = T extends { __cache?: infer R } ? Value extends R ? 1 : 2 : 3
function add<
S extends Set<Value> | Set<Value> & { __cache?: Value },
Vals extends Value
>(set: S, value: [...Vals], ...flag: Check<S, Vals> extends 1 ? [never] : []): UpdateProp<S, Vals>
function add<S extends Set<Value> & { __cache?: any }, Vals extends Value>
(set: S, value: [...Vals]) {
return set.add(value)
}
const result = add(new Set<Value>(), [1, 2]) // ok
const result2 = add(result, [3,4]) // ok
const result3 = add(result2, [3,4]) // expected error
const result4 = add(result2, [4,3]) // ok
Playground
If you still want to use Set in normal way, like all people ))
you can extend Set class:
class MySet<T extends [number, number]> extends Set<T>{
override add(val: T) {
const [x, y] = val
const keys = [...this.keys()];
const exists = keys.findIndex(([a, b]) => a === x && b === y)
if (exists === -1) {
return this
}
super.add(val)
return this
}
}
I have used override keyword which is new in TS 4.3. Feel free to get rid of it if you have lower version of TS
Playground 2