mirror of
https://github.com/discordjs/discord.js.git
synced 2026-06-01 08:30:08 +00:00
fix(collection): preserve ReadonlyCollection through tap/each (#11501)
* fix(collection): preserve ReadonlyCollection through tap/each
`each` and `tap` return polymorphic `this`, which TypeScript resolves
against the `Omit<Collection, ...>` portion of `ReadonlyCollection`
rather than the full intersection. That let callers reach `set` and
`delete` on the result of a chain started from a `ReadonlyCollection`:
const ro: ReadonlyCollection<string, number> = new Collection(...);
ro.tap(() => {}).set('x', 0); // compiled, mutated the underlying Map
The fix omits `each` and `tap` from the base `Omit` and re-declares
them on the `ReadonlyCollection` side of the intersection so the return
type narrows back to `ReadonlyCollection`.
Closes #10514
* test(collection): gate readonly-chain checks behind if(false)
Previously the `@ts-expect-error` lines still executed the `set` and
`delete` mutations at runtime, and the final `size === 1` passed only
because they happened to cancel out. Wrapping the assertions in
`if (false)` keeps the compile-time guarantee while the backing
collection is truly untouched, and adds a `get('a') === 1` check as
a belt.
* test(collection): move readonly type checks to *.test-d.ts
Addresses review feedback. The type-level assertions around tap() and
each() preserving ReadonlyCollection belong in a *.test-d.ts file so
they run through vitest's typecheck pass instead of runtime.
Replaces the if(false)-gated @ts-expect-error block in collection.test.ts
with expectTypeOf assertions in a new collection.test-d.ts. Covers both
the no-thisArg and with-thisArg overloads of tap and each.
This commit is contained in:
16
packages/collection/__tests__/collection.test-d.ts
Normal file
16
packages/collection/__tests__/collection.test-d.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { expectTypeOf, test } from 'vitest';
|
||||
import { Collection, type ReadonlyCollection } from '../src/index.js';
|
||||
|
||||
test('ReadonlyCollection#tap preserves the readonly type', () => {
|
||||
const readonly: ReadonlyCollection<string, number> = new Collection([['a', 1]]);
|
||||
|
||||
expectTypeOf(readonly.tap(() => {})).toEqualTypeOf<ReadonlyCollection<string, number>>();
|
||||
expectTypeOf(readonly.tap(() => {}, null)).toEqualTypeOf<ReadonlyCollection<string, number>>();
|
||||
});
|
||||
|
||||
test('ReadonlyCollection#each preserves the readonly type', () => {
|
||||
const readonly: ReadonlyCollection<string, number> = new Collection([['a', 1]]);
|
||||
|
||||
expectTypeOf(readonly.each(() => {})).toEqualTypeOf<ReadonlyCollection<string, number>>();
|
||||
expectTypeOf(readonly.each(() => {}, null)).toEqualTypeOf<ReadonlyCollection<string, number>>();
|
||||
});
|
||||
@@ -5,9 +5,22 @@
|
||||
*/
|
||||
export type ReadonlyCollection<Key, Value> = Omit<
|
||||
Collection<Key, Value>,
|
||||
keyof Map<Key, Value> | 'ensure' | 'reverse' | 'sort' | 'sweep'
|
||||
keyof Map<Key, Value> | 'each' | 'ensure' | 'reverse' | 'sort' | 'sweep' | 'tap'
|
||||
> &
|
||||
ReadonlyMap<Key, Value>;
|
||||
ReadonlyMap<Key, Value> & {
|
||||
each(
|
||||
fn: (value: Value, key: Key, collection: ReadonlyCollection<Key, Value>) => void,
|
||||
): ReadonlyCollection<Key, Value>;
|
||||
each<This>(
|
||||
fn: (this: This, value: Value, key: Key, collection: ReadonlyCollection<Key, Value>) => void,
|
||||
thisArg: This,
|
||||
): ReadonlyCollection<Key, Value>;
|
||||
tap(fn: (collection: ReadonlyCollection<Key, Value>) => void): ReadonlyCollection<Key, Value>;
|
||||
tap<This>(
|
||||
fn: (this: This, collection: ReadonlyCollection<Key, Value>) => void,
|
||||
thisArg: This,
|
||||
): ReadonlyCollection<Key, Value>;
|
||||
};
|
||||
|
||||
export interface Collection<Key, Value> {
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user