Skip to content
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

Improve Typescript definition for Map #1841

Merged
merged 32 commits into from Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
47c78b4
try implementing TS Map as object definition
jdeniau Jun 29, 2021
0543432
working version of ObjectLikeMap with `.get()`
jdeniau Jul 2, 2021
b5d874a
start `getIn` implementation for ObjectLikeMap
jdeniau Jul 7, 2021
8cf0983
try to fix and add set definition
jdeniau Jul 15, 2021
f7c1f33
fix getIn using variadic tuple types
mbullington Jul 16, 2021
9ec983f
try implementing TS Map as object definition
jdeniau Jun 29, 2021
4d248af
working version of ObjectLikeMap with `.get()`
jdeniau Jul 2, 2021
720b847
start `getIn` implementation for ObjectLikeMap
jdeniau Jul 7, 2021
c762557
try to fix and add set definition
jdeniau Jul 15, 2021
3705794
Merge branch 'jd-feat-tsMapObject' of github.com:jdeniau/immutable-js…
mbullington Jul 19, 2021
82e957c
Merge remote-tracking branch 'origin/main' into jd-feat-tsMapObject
jdeniau Oct 15, 2021
43f6590
fix linting
jdeniau Oct 15, 2021
72f60e9
Remove weird comment
jdeniau Oct 15, 2021
b58b9d6
Fix website build
jdeniau Oct 15, 2021
b0faf97
add test for `.set`
jdeniau Oct 15, 2021
42170d6
ObjectLikeMap → MapFromObject
jdeniau Oct 21, 2021
790773a
Update CHANGELOG
jdeniau Oct 21, 2021
3e160f8
MapFromObject: add `remove` and `delete` + better type definition for…
jdeniau Oct 27, 2021
a26d4ec
be consistant with standard objects
jdeniau Oct 27, 2021
d112b0c
reset Map and OrderedMap return to `this` due to latest change on this
jdeniau Oct 27, 2021
f225394
ignore ConditionalType in website generation
jdeniau Oct 28, 2021
89b83a0
Merge branch 'main' into jd-feat-tsMapObject
jdeniau Oct 28, 2021
0d57333
Merge remote-tracking branch 'origin/main' into jd-feat-tsMapObject
jdeniau Nov 12, 2021
d627ad3
implement `.update` thanks to @mbullington
jdeniau Nov 12, 2021
0d5d6c8
fox get + add some tests
jdeniau Nov 21, 2021
ee1eca1
Make sure merge works properly with MapFromObject
mbullington Dec 1, 2021
47be7ee
fix update method
mbullington Dec 1, 2021
0e9da4d
Merge remote-tracking branch 'origin/main' into jd-feat-tsMapObject
jdeniau Sep 28, 2022
ad0356b
Merge remote-tracking branch 'origin/main' into jd-feat-tsMapObject
jdeniau Feb 10, 2023
beeac64
Rename `MapFromObject` to `MapOf` to match `RecordOf`
jdeniau Feb 10, 2023
def6f09
Merge remote-tracking branch 'origin/main' into jd-feat-tsMapObject
jdeniau Aug 23, 2023
c302576
implement toJS and toJSON due to recent fixes in main branch
jdeniau Aug 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions __tests__/Map.ts
Expand Up @@ -60,8 +60,11 @@ describe('Map', () => {
const l = List([List(['a', 'A']), List(['b', 'B']), List(['c', 'C'])]);
const m = Map(l);
expect(m.size).toBe(3);
// @ts-expect-error -- Not supported by typescript since 4.0.0 https://github.com/immutable-js/immutable-js/pull/1626
expect(m.get('a')).toBe('A');
// @ts-expect-error -- Not supported by typescript since 4.0.0 https://github.com/immutable-js/immutable-js/pull/1626
expect(m.get('b')).toBe('B');
// @ts-expect-error -- Not supported by typescript since 4.0.0 https://github.com/immutable-js/immutable-js/pull/1626
expect(m.get('c')).toBe('C');
Comment on lines +63 to 68
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused - is this not currently failing? Why does this PR cause this to start producing errors?

});

Expand Down Expand Up @@ -97,6 +100,7 @@ describe('Map', () => {
it('accepts non-collection array-like objects as keyed collections', () => {
const m = Map({ length: 3, 1: 'one' });
expect(m.get('length')).toBe(3);
// @ts-expect-error -- type error, but the API is tolerante
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be a type error. Similarly, we should ensure that m.get(1) is well typed to return NSV (or be a type error if that arg is omitted)

I filed an issue about this a while back and it was closed as "Working as intended" despite being clearly incorrect. 🤷 - microsoft/TypeScript#45170 (comment)

A workaround type to fix this looks like:

type ObjectKeys<T> = { [K in keyof T]: K extends number ? `${K}` : K }[keyof T]

expect(m.get('1')).toBe('one');
expect(m.toJS()).toEqual({ length: 3, 1: 'one' });
});
Expand Down
12 changes: 6 additions & 6 deletions __tests__/merge.ts
Expand Up @@ -23,8 +23,8 @@ describe('merge', () => {
});

it('merges two maps with a merge function', () => {
const m1 = Map({ a: 1, b: 2, c: 3 });
const m2 = Map({ d: 10, b: 20, e: 30 });
const m1 = Map<string, number>({ a: 1, b: 2, c: 3 });
const m2 = Map<string, number>({ d: 10, b: 20, e: 30 });
expect(m1.mergeWith((a: any, b: any) => a + b, m2)).toEqual(
Map({ a: 1, b: 22, c: 3, d: 10, e: 30 })
);
Expand All @@ -38,8 +38,8 @@ describe('merge', () => {
});

it('provides key as the third argument of merge function', () => {
const m1 = Map({ id: 'temp', b: 2, c: 3 });
const m2 = Map({ id: 10, b: 20, e: 30 });
const m1 = Map<string, string | number>({ id: 'temp', b: 2, c: 3 });
const m2 = Map<string, number>({ id: 10, b: 20, e: 30 });
const add = (a: any, b: any) => a + b;
expect(
m1.mergeWith((a, b, key) => (key !== 'id' ? add(a, b) : b), m2)
Expand Down Expand Up @@ -152,12 +152,12 @@ describe('merge', () => {

it('merges map entries with List and Set values', () => {
const initial = Map({
a: Map({ x: 10, y: 20 }),
a: Map<string, number>({ x: 10, y: 20 }),
b: List([1, 2, 3]),
c: Set([1, 2, 3]),
});
const additions = Map({
a: Map({ y: 50, z: 100 }),
a: Map<string, number>({ y: 50, z: 100 }),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need an explicit type? Shouldn't MapFromObject<{ x: 10, y: 20 }> extend from Map<string, number>?

The code written here without explicit types looks correct, and I'd be surprised to find a type error

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar feedback for all of the tests in this - are we missing something about the merge function's type?

b: List([4, 5, 6]),
c: Set([4, 5, 6]),
});
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -63,7 +63,7 @@
"lint:js": "eslint \"{__tests__,src,website/src}/**/*.js\"",
"lint:ts": "tslint --project type-definitions/tsconfig.json \"{__tests__,src,type-definitions,website/src}/**/*.{ts,tsx}\"",
"type-check": "run-s type-check:*",
"type-check:ts": "tsc --project type-definitions/tsconfig.json && tsc --project __tests__/tsconfig.json && dtslint type-definitions/ts-tests",
"type-check:ts": "tsc --project type-definitions/tsconfig.json && tsc --project __tests__/tsconfig.json && dtslint type-definitions/ts-tests --localTs node_modules/typescript/lib",
"type-check:flow": "flow check type-definitions/flow-tests --include-warnings",
"build": "run-s build:*",
"build:clean": "rimraf dist",
Expand Down
68 changes: 68 additions & 0 deletions type-definitions/immutable.d.ts
Expand Up @@ -760,9 +760,77 @@ declare namespace Immutable {
* not altered.
*/
function Map<K, V>(collection?: Iterable<[K, V]>): Map<K, V>;
function Map<R extends { [key in string | number]: unknown }>(
obj: R
): ObjectLikeMap<R>;
function Map<V>(obj: { [key: string]: V }): Map<string, V>;
function Map<K extends string, V>(obj: { [P in K]?: V }): Map<K, V>;

/**
* Represent a Map constructed by an object
*
* @ignore
*/
interface ObjectLikeMap<R extends { [key in string | number]: unknown }>
extends Map<keyof R, R[keyof R]> {
/**
* Returns the value associated with the provided key, or notSetValue if
* the Collection does not contain this key.
*
* Note: it is possible a key may be associated with an `undefined` value,
* so if `notSetValue` is not provided and this method returns `undefined`,
* that does not guarantee the key was not found.
*/
get<K extends keyof R>(key: K, notSetValue?: unknown): R[K];
get<NSV>(key: string, notSetValue: NSV): NSV;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be key: any here? K alone can be string | number | symbol, but beyond I think we would expect that for any requested key that doesn't exist, we ask a NSV to be provided


// https://github.com/microsoft/TypeScript/pull/39094
getIn<P extends ReadonlyArray<string | number>>(
searchKeyPath: [...P],
notSetValue?: unknown
): RetrievePath<R, P>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of what has made typing getIn correctly challenging is that we don't necessarily know the nested types within. The recursive type you've defined here gets way closer, but if it's correct it needs to handle all possible cases that getIn operates on in the nested layers. Then there's no reason it can't be used in all getIn methods


set<K extends string | number, V>(
key: K,
value: V
): this & ObjectLikeMap<{ [key in K]: V }>;
}

// Loosely based off of this work.
// https://github.com/immutable-js/immutable-js/issues/1462#issuecomment-584123268

/** @ignore */
type GetMapType<S> = S extends ObjectLikeMap<infer T> ? T : S;

/** @ignore */
type Head<T extends ReadonlyArray<any>> = T extends [
infer H,
...Array<unknown>
]
? H
: never;

/** @ignore */
type Tail<T extends ReadonlyArray<any>> = T extends [unknown, ...infer I]
? I
: Array<never>;

/** @ignore */
type RetrievePathReducer<
T,
C,
L extends ReadonlyArray<any>
> = C extends keyof GetMapType<T>
? L extends []
? GetMapType<T>[C]
: RetrievePathReducer<GetMapType<T>[C], Head<L>, Tail<L>>
: never;

/** @ignore */
type RetrievePath<R, P extends ReadonlyArray<string | number>> = P extends []
? P
: RetrievePathReducer<R, Head<P>, Tail<P>>;

interface Map<K, V> extends Collection.Keyed<K, V> {
/**
* The number of entries in this Map.
Expand Down
2 changes: 1 addition & 1 deletion type-definitions/ts-tests/covariance.ts
Expand Up @@ -34,7 +34,7 @@ let listOfC: List<C> = listOfB;
declare var mapOfB: Map<string, B>;
let mapOfA: Map<string, A> = mapOfB;

// $ExpectType Map<string, B>
// $ExpectType ObjectLikeMap<{ b: B; }>
mapOfA = Map({ b: new B() });

// $ExpectError
Expand Down
70 changes: 54 additions & 16 deletions type-definitions/ts-tests/map.ts
Expand Up @@ -16,13 +16,23 @@ import { Map, List } from 'immutable';
Map([['a', 'a']]);

// $ExpectType Map<number, string>
Map(
List<[number, string]>([[1, 'a']])
);
Map(List<[number, string]>([[1, 'a']]));

// $ExpectType Map<string, number>
// $ExpectType ObjectLikeMap<{ a: number; }>
Map({ a: 1 });

// $ExpectType ObjectLikeMap<{ a: number; b: string; }>
Map({ a: 1, b: 'b' });

// $ExpectType ObjectLikeMap<{ a: ObjectLikeMap<{ b: ObjectLikeMap<{ c: number; }>; }>; }>
Map({ a: Map({ b: Map({ c: 3 }) }) });

// $ExpectError
Map<{ a: string }>({ a: 1 });

// $ExpectError
Map<{ a: string }>({ a: 'a', b: 'b' });

// No longer works in typescript@>=3.9
// // $ExpectError - TypeScript does not support Lists as tuples
// Map(List([List(['a', 'b'])]));
Expand Down Expand Up @@ -61,6 +71,31 @@ import { Map, List } from 'immutable';

// $ExpectError
Map<number, number>().get<number>(4, 'a');

// $ExpectType number
Map({ a: 4, b: true }).get('a');

// $ExpectType boolean
Map({ a: 4, b: true }).get('b');

// $ExpectType boolean
Map({ a: Map({ b: true }) })
.get('a')
.get('b');

// $ExpectError
Map({ a: 4 }).get('b');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add .get('b', undefined) to test the alternate NSV definition?

}
jdeniau marked this conversation as resolved.
Show resolved Hide resolved

{
// Minimum TypeScript Version: 4.1
// #getIn

// $ExpectType number
Map({ a: 4, b: true }).getIn(['a']);

// $ExpectType number
Map({ a: Map({ b: Map({ c: Map({ d: 4 }) }) }) }).getIn(['a', 'b', 'c', 'd']);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add getIn tests where nested Map are constructed by other means (eg not only ObjectFromMap type?) as well as having List within?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunatly, it's only working for MapFromObject for now.


{
Expand All @@ -80,6 +115,15 @@ import { Map, List } from 'immutable';

// $ExpectType Map<number, string | number>
Map<number, number | string>().set(0, 'a');

// $ExpectType ObjectLikeMap<{ a: number; }> & ObjectLikeMap<{ b: string; }>
Map({ a: 1 }).set('b', 'b');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised by this, I'd expect either MapFromObject<{ a: 1, b: 'b' }> or Map<string, number | string> as a result?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above about the consistency with plain objects in TS


// $ExpectType number
Map({ a: 1 }).set('b', 'b').get('a');

// $ExpectType string
Map({ a: 1 }).set('b', 'b').get('b');
}

{
Expand Down Expand Up @@ -280,20 +324,14 @@ import { Map, List } from 'immutable';
// #flatMap

// $ExpectType Map<number, number>
Map<
number,
number
>().flatMap((value: number, key: number, iter: Map<number, number>) => [
[0, 1],
]);
Map<number, number>().flatMap(
(value: number, key: number, iter: Map<number, number>) => [[0, 1]]
);

// $ExpectType Map<string, string>
Map<
number,
number
>().flatMap((value: number, key: number, iter: Map<number, number>) => [
['a', 'b'],
]);
Map<number, number>().flatMap(
(value: number, key: number, iter: Map<number, number>) => [['a', 'b']]
);

// $ExpectType Map<number, number>
Map<number, number>().flatMap<number, number>(
Expand Down
3 changes: 3 additions & 0 deletions website/src/static/getTypeDefs.ts
Expand Up @@ -709,6 +709,9 @@ function typesVisitor(source: ts.SourceFile) {
],
};
}
case ts.SyntaxKind.RestType: {
return { k: TypeKind.Never };
}
}
throw new Error('Unknown type kind: ' + ts.SyntaxKind[node.kind]);
}
Expand Down