Skip to content

Update functional types #2068

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions __tests__/updateIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ describe('updateIn', () => {
it('deep edit in raw JS', () => {
const m = { a: { b: { c: [10] } } };
expect(
// @ts-expect-error -- `updateIn` should copy the comportment of `getIn`
updateIn(m, ['a', 'b', 'c', 0], (value: number) => value * 2)
).toEqual({
a: { b: { c: [20] } },
Expand Down Expand Up @@ -197,7 +196,6 @@ describe('updateIn', () => {
m,
['a', 'b', 'z'],
Map<string, number>(),
// @ts-expect-error -- updateIn should handle the `notSetValue` parameter
(map: Map<string, number>) => map.set('d', 20)
)
).toEqual({ a: { b: { c: 10, z: Map({ d: 20 }) } } });
Expand All @@ -223,7 +221,6 @@ describe('updateIn', () => {

it('update with notSetValue when non-existing key in raw JS', () => {
const m = { a: { b: { c: 10 } } };
// @ts-expect-error -- updateIn should handle the `notSetValue` parameter
expect(updateIn(m, ['x'], 100, (map: number) => map + 1)).toEqual({
a: { b: { c: 10 } },
x: 101,
Expand Down
115 changes: 92 additions & 23 deletions type-definitions/immutable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ declare namespace Immutable {
: T extends Collection.Keyed<infer KeyedKey, infer V>
? // convert KeyedCollection to DeepCopy plain JS object
{
[key in KeyedKey extends string | number | symbol
[key in KeyedKey extends PropertyKey
? KeyedKey
: string]: V extends object ? unknown : V;
}
Expand Down Expand Up @@ -853,9 +853,7 @@ declare namespace Immutable {
* not altered.
*/
function Map<K, V>(collection?: Iterable<[K, V]>): Map<K, V>;
function Map<R extends { [key in string | number | symbol]: unknown }>(
obj: R
): MapOf<R>;
function Map<R extends { [key in PropertyKey]: unknown }>(obj: R): MapOf<R>;
function Map<V>(obj: { [key: string]: V }): Map<string, V>;
function Map<K extends string | symbol, V>(obj: { [P in K]?: V }): Map<K, V>;

Expand All @@ -864,7 +862,7 @@ declare namespace Immutable {
*
* @ignore
*/
interface MapOf<R extends { [key in string | number | symbol]: unknown }>
interface MapOf<R extends { [key in PropertyKey]: unknown }>
extends Map<keyof R, R[keyof R]> {
/**
* Returns the value associated with the provided key, or notSetValue if
Expand Down Expand Up @@ -3155,14 +3153,14 @@ declare namespace Immutable {
*
* Converts keys to Strings.
*/
toJS(): { [key in string | number | symbol]: DeepCopy<V> };
toJS(): { [key in PropertyKey]: DeepCopy<V> };

/**
* Shallowly converts this Keyed Seq to equivalent native JavaScript Object.
*
* Converts keys to Strings.
*/
toJSON(): { [key in string | number | symbol]: V };
toJSON(): { [key in PropertyKey]: V };

/**
* Shallowly converts this collection to an Array.
Expand Down Expand Up @@ -3763,14 +3761,14 @@ declare namespace Immutable {
*
* Converts keys to Strings.
*/
toJS(): { [key in string | number | symbol]: DeepCopy<V> };
toJS(): { [key in PropertyKey]: DeepCopy<V> };

/**
* Shallowly converts this Keyed collection to equivalent native JavaScript Object.
*
* Converts keys to Strings.
*/
toJSON(): { [key in string | number | symbol]: V };
toJSON(): { [key in PropertyKey]: V };

/**
* Shallowly converts this collection to an Array.
Expand Down Expand Up @@ -4520,17 +4518,15 @@ declare namespace Immutable {
* `Collection.Indexed`, and `Collection.Set` become `Array`, while
* `Collection.Keyed` become `Object`, converting keys to Strings.
*/
toJS():
| Array<DeepCopy<V>>
| { [key in string | number | symbol]: DeepCopy<V> };
toJS(): Array<DeepCopy<V>> | { [key in PropertyKey]: DeepCopy<V> };

/**
* Shallowly converts this Collection to equivalent native JavaScript Array or Object.
*
* `Collection.Indexed`, and `Collection.Set` become `Array`, while
* `Collection.Keyed` become `Object`, converting keys to Strings.
*/
toJSON(): Array<V> | { [key in string | number | symbol]: V };
toJSON(): Array<V> | { [key in PropertyKey]: V };

/**
* Shallowly converts this collection to an Array.
Expand Down Expand Up @@ -5749,9 +5745,12 @@ declare namespace Immutable {
key: K,
notSetValue: unknown
): C[K];
function get<V>(collection: { [key: string]: V }, key: string): V | undefined;
function get<V>(
collection: { [key: PropertyKey]: V },
key: string
): V | undefined;
function get<V, NSV>(
collection: { [key: string]: V },
collection: { [key: PropertyKey]: V },
key: string,
notSetValue: NSV
): V | NSV;
Expand Down Expand Up @@ -5971,7 +5970,11 @@ declare namespace Immutable {
* hasIn({ x: { y: { z: 123 }}}, ['x', 'q', 'p']) // false
* ```
*/
function hasIn(collection: unknown, keyPath: Iterable<unknown>): boolean;
function hasIn(
collection: string | boolean | number,
keyPath: KeyPath<unknown>
): never;
function hasIn<K>(collection: unknown, keyPath: KeyPath<K>): boolean;

/**
* Returns a copy of the collection with the value at the key path removed.
Expand Down Expand Up @@ -6025,17 +6028,83 @@ declare namespace Immutable {
* console.log(original) // { x: { y: { z: 123 }}}
* ```
*/
function updateIn<C>(
function updateIn<K extends PropertyKey, V, C extends Collection<K, V>>(
collection: C,
keyPath: Iterable<unknown>,
updater: (value: unknown) => unknown
keyPath: KeyPath<K>,
updater: (
value: RetrievePath<C, Array<K>> | undefined
) => unknown | undefined
): C;
function updateIn<C>(
function updateIn<K extends PropertyKey, V, C extends Collection<K, V>, NSV>(
collection: C,
keyPath: Iterable<unknown>,
notSetValue: unknown,
updater: (value: unknown) => unknown
keyPath: KeyPath<K>,
notSetValue: NSV,
updater: (value: RetrievePath<C, Array<K>> | NSV) => unknown
): C;
function updateIn<
TProps extends object,
C extends Record<TProps>,
K extends keyof TProps,
>(
record: C,
keyPath: KeyPath<K>,
updater: (value: RetrievePath<C, Array<K>>) => unknown
): C;
function updateIn<
TProps extends object,
C extends Record<TProps>,
K extends keyof TProps,
NSV,
>(
record: C,
keyPath: KeyPath<K>,
notSetValue: NSV,
updater: (value: RetrievePath<C, Array<K>> | NSV) => unknown
): C;
function updateIn<K extends PropertyKey, V, C extends Array<V>>(
collection: Array<V>,
keyPath: KeyPath<string | number>,
updater: (
value: RetrievePath<C, Array<K>> | undefined
) => unknown | undefined
): Array<V>;
function updateIn<K extends PropertyKey, V, C extends Array<V>, NSV>(
collection: Array<V>,
keyPath: KeyPath<K>,
notSetValue: NSV,
updater: (value: RetrievePath<C, Array<K>> | NSV) => unknown
): Array<V>;
function updateIn<K extends PropertyKey, C>(
object: C,
keyPath: KeyPath<K>,
updater: (value: RetrievePath<C, Array<K>>) => unknown
): C;
function updateIn<K extends PropertyKey, C, NSV>(
object: C,
keyPath: KeyPath<K>,
notSetValue: NSV,
updater: (value: RetrievePath<C, Array<K>> | NSV) => unknown
): C;
function updateIn<
K extends PropertyKey,
V,
C extends { [key: PropertyKey]: V },
>(
collection: C,
keyPath: KeyPath<K>,
updater: (value: RetrievePath<C, Array<K>>) => unknown
): { [key: PropertyKey]: V };
function updateIn<
K extends PropertyKey,
V,
C extends { [key: PropertyKey]: V },
NSV,
>(
collection: C,
keyPath: KeyPath<K>,
notSetValue: NSV,
updater: (value: RetrievePath<C, Array<K>> | NSV) => unknown
): { [key: PropertyKey]: V };

/**
* Returns a copy of the collection with the remaining collections merged in.
Expand Down
46 changes: 46 additions & 0 deletions type-definitions/ts-tests/functional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
get,
getIn,
has,
hasIn,
set,
remove,
update,
Expand Down Expand Up @@ -84,6 +85,51 @@ test('has', () => {
expect(has({ x: 10, y: 20 }, 'x')).type.toBeBoolean();
});

test('hasIn', () => {
expect(hasIn('a', ['length' as const])).type.toBe<never>();

expect(hasIn(123, [])).type.toBe<never>();

expect(hasIn(true, [])).type.toBe<never>();

expect(hasIn([1, 2, 3], [0])).type.toBe<boolean>();

// first parameter type is Array<number> so we can not detect that the number will be invalid
expect(hasIn([1, 2, 3], [99])).type.toBe<boolean>();

// We do not handle List in hasIn TS type yet (hard to convert to a tuple)
expect(hasIn([1, 2, 3], List([0]))).type.toBe<boolean>();

expect(hasIn(List([1, 2, 3]), [0])).type.toBe<boolean>();

// first parameter type is Array<number> so we can not detect that the number will be invalid
expect(hasIn(List([1, 2, 3]), [99])).type.toBe<boolean>();

expect(hasIn(List([1, 2, 3]), ['a' as const])).type.toBe<boolean>();

expect(hasIn({ x: 10, y: 20 }, ['x' as const])).type.toBe<boolean>();

expect(hasIn({ x: { y: 20 } }, ['z' as const])).type.toBe<boolean>();

expect(
hasIn({ x: { y: 20 } }, ['x' as const, 'y' as const])
).type.toBe<boolean>();

expect(
hasIn({ x: Map({ y: 20 }) }, ['x' as const, 'y' as const])
).type.toBe<boolean>();

expect(
hasIn(Map({ x: Map({ y: 20 }) }), ['x' as const, 'y' as const])
).type.toBe<boolean>();

const o = Map({ x: List([Map({ y: 20 })]) });

expect(hasIn(o, ['x' as const, 'y' as const])).type.toBe<boolean>();

expect(hasIn(o, ['x' as const, 0, 'y' as const])).type.toBe<boolean>();
});

test('set', () => {
expect(set([1, 2, 3], 0, 10)).type.toBe<number[]>();

Expand Down