Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 84e360f

Browse filesBrowse files
authored
Better getIn TypeScript RetrievePath (#2070)
* Better getIn TS RetrievePath * o not drop support for TS 4.x now * rollback to old "as const" hell
1 parent f23809a commit 84e360f
Copy full SHA for 84e360f

File tree

Expand file treeCollapse file tree

3 files changed

+120
-23
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+120
-23
lines changed

‎type-definitions/immutable.d.ts

Copy file name to clipboardExpand all lines: type-definitions/immutable.d.ts
+48-18Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ declare namespace Immutable {
171171
*/
172172
export type Comparator<T> = (left: T, right: T) => PairSorting | number;
173173

174+
/**
175+
* @ignore
176+
*
177+
* KeyPath allowed for `xxxIn` methods
178+
*/
179+
export type KeyPath<K> = OrderedCollection<K> | ArrayLike<K>;
180+
174181
/**
175182
* Lists are ordered indexed dense collections, much like a JavaScript
176183
* Array.
@@ -873,7 +880,7 @@ declare namespace Immutable {
873880
// TODO `<const P extends ...>` can be used after dropping support for TypeScript 4.x
874881
// reference: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#const-type-parameters
875882
// after this change, `as const` assertions can be remove from the type tests
876-
getIn<P extends ReadonlyArray<string | number | symbol>>(
883+
getIn<P extends ReadonlyArray<PropertyKey>>(
877884
searchKeyPath: [...P],
878885
notSetValue?: unknown
879886
): RetrievePath<R, P>;
@@ -905,8 +912,14 @@ declare namespace Immutable {
905912
// Loosely based off of this work.
906913
// https://github.com/immutable-js/immutable-js/issues/1462#issuecomment-584123268
907914

908-
/** @ignore */
909-
type GetMapType<S> = S extends MapOf<infer T> ? T : S;
915+
/**
916+
* @ignore
917+
* Convert an immutable type to the equivalent plain TS type
918+
* - MapOf -> object
919+
* - List -> Array
920+
*/
921+
type GetNativeType<S> =
922+
S extends MapOf<infer T> ? T : S extends List<infer I> ? Array<I> : S;
910923

911924
/** @ignore */
912925
type Head<T extends ReadonlyArray<unknown>> = T extends [
@@ -915,28 +928,32 @@ declare namespace Immutable {
915928
]
916929
? H
917930
: never;
918-
919931
/** @ignore */
920932
type Tail<T extends ReadonlyArray<unknown>> = T extends [unknown, ...infer I]
921933
? I
922934
: Array<never>;
923-
924935
/** @ignore */
925936
type RetrievePathReducer<
926937
T,
927938
C,
928939
L extends ReadonlyArray<unknown>,
929-
> = C extends keyof GetMapType<T>
930-
? L extends []
931-
? GetMapType<T>[C]
932-
: RetrievePathReducer<GetMapType<T>[C], Head<L>, Tail<L>>
933-
: never;
940+
NT = GetNativeType<T>,
941+
> =
942+
// we can not retrieve a path from a primitive type
943+
T extends string | number | boolean | null | undefined
944+
? never
945+
: C extends keyof NT
946+
? L extends [] // L extends [] means we are at the end of the path, lets return the current type
947+
? NT[C]
948+
: // we are not at the end of the path, lets continue with the next key
949+
RetrievePathReducer<NT[C], Head<L>, Tail<L>>
950+
: // C is not a "key" of NT, so the path is invalid
951+
never;
934952

935953
/** @ignore */
936-
type RetrievePath<
937-
R,
938-
P extends ReadonlyArray<string | number | symbol>,
939-
> = P extends [] ? P : RetrievePathReducer<R, Head<P>, Tail<P>>;
954+
type RetrievePath<R, P extends ReadonlyArray<PropertyKey>> = P extends []
955+
? P
956+
: RetrievePathReducer<R, Head<P>, Tail<P>>;
940957

941958
interface Map<K, V> extends Collection.Keyed<K, V> {
942959
/**
@@ -5908,6 +5925,9 @@ declare namespace Immutable {
59085925
updater: (value: V | NSV) => V
59095926
): { [key: string]: V };
59105927

5928+
// TODO `<const P extends ...>` can be used after dropping support for TypeScript 4.x
5929+
// reference: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#const-type-parameters
5930+
// after this change, `as const` assertions can be remove from the type tests
59115931
/**
59125932
* Returns the value at the provided key path starting at the provided
59135933
* collection, or notSetValue if the key path is not defined.
@@ -5922,10 +5942,20 @@ declare namespace Immutable {
59225942
* getIn({ x: { y: { z: 123 }}}, ['x', 'q', 'p'], 'ifNotSet') // 'ifNotSet'
59235943
* ```
59245944
*/
5925-
function getIn(
5926-
collection: unknown,
5927-
keyPath: Iterable<unknown>,
5928-
notSetValue?: unknown
5945+
function getIn<C, P extends ReadonlyArray<PropertyKey>>(
5946+
object: C,
5947+
keyPath: [...P]
5948+
): RetrievePath<C, P>;
5949+
function getIn<C, P extends KeyPath<unknown>>(object: C, keyPath: P): unknown;
5950+
function getIn<C, P extends ReadonlyArray<PropertyKey>, NSV>(
5951+
collection: C,
5952+
keyPath: [...P],
5953+
notSetValue: NSV
5954+
): RetrievePath<C, P> extends never ? NSV : RetrievePath<C, P>;
5955+
function getIn<C, P extends KeyPath<unknown>, NSV>(
5956+
object: C,
5957+
keyPath: P,
5958+
notSetValue: NSV
59295959
): unknown;
59305960

59315961
/**

‎type-definitions/ts-tests/functional.ts

Copy file name to clipboardExpand all lines: type-definitions/ts-tests/functional.ts
+68-1Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import { expect, test } from 'tstyche';
2-
import { get, has, set, remove, update } from 'immutable';
2+
import {
3+
get,
4+
getIn,
5+
has,
6+
set,
7+
remove,
8+
update,
9+
Map,
10+
List,
11+
MapOf,
12+
} from 'immutable';
313

414
test('get', () => {
515
expect(get([1, 2, 3], 0)).type.toBe<number | undefined>();
@@ -11,6 +21,63 @@ test('get', () => {
1121
expect(get({ x: 10, y: 20 }, 'z', 'missing')).type.toBe<number | 'missing'>();
1222
});
1323

24+
test('getIn', () => {
25+
expect(getIn('a', ['length' as const])).type.toBe<never>();
26+
27+
expect(getIn([1, 2, 3], [0])).type.toBe<number>();
28+
29+
// first parameter type is Array<number> so we can not detect that the number will be invalid
30+
expect(getIn([1, 2, 3], [99])).type.toBe<number>();
31+
32+
// We do not handle List in getIn TS type yet (hard to convert to a tuple)
33+
expect(getIn([1, 2, 3], List([0]))).type.toBe<unknown>();
34+
35+
expect(getIn([1, 2, 3], [0], 'a' as const)).type.toBe<number>();
36+
37+
expect(getIn(List([1, 2, 3]), [0])).type.toBe<number>();
38+
39+
// first parameter type is Array<number> so we can not detect that the number will be invalid
40+
expect(getIn(List([1, 2, 3]), [99])).type.toBe<number>();
41+
42+
expect(getIn(List([1, 2, 3]), ['a' as const])).type.toBe<never>();
43+
44+
expect(
45+
getIn(List([1, 2, 3]), ['a' as const], 'missing')
46+
).type.toBe<'missing'>();
47+
48+
expect(getIn({ x: 10, y: 20 }, ['x' as const])).type.toBe<number>();
49+
50+
expect(
51+
getIn({ x: 10, y: 20 }, ['z' as const], 'missing')
52+
).type.toBe<'missing'>();
53+
54+
expect(getIn({ x: { y: 20 } }, ['x' as const])).type.toBe<{ y: number }>();
55+
56+
expect(getIn({ x: { y: 20 } }, ['z' as const])).type.toBe<never>();
57+
58+
expect(
59+
getIn({ x: { y: 20 } }, ['x' as const, 'y' as const])
60+
).type.toBe<number>();
61+
62+
expect(
63+
getIn({ x: Map({ y: 20 }) }, ['x' as const, 'y' as const])
64+
).type.toBe<number>();
65+
66+
expect(
67+
getIn(Map({ x: Map({ y: 20 }) }), ['x' as const, 'y' as const])
68+
).type.toBe<number>();
69+
70+
const o = Map({ x: List([Map({ y: 20 })]) });
71+
72+
expect(getIn(o, ['x' as const, 'y' as const])).type.toBe<never>();
73+
74+
expect(getIn(o, ['x' as const])).type.toBe<List<MapOf<{ y: number }>>>();
75+
76+
expect(getIn(o, ['x' as const, 0])).type.toBe<MapOf<{ y: number }>>();
77+
78+
expect(getIn(o, ['x' as const, 0, 'y' as const])).type.toBe<number>();
79+
});
80+
1481
test('has', () => {
1582
expect(has([1, 2, 3], 0)).type.toBeBoolean();
1683

‎type-definitions/ts-tests/map.ts

Copy file name to clipboardExpand all lines: type-definitions/ts-tests/map.ts
+4-4Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ test('#get', () => {
8585
});
8686

8787
test('#getIn', () => {
88-
const result = Map({ a: 4, b: true }).getIn(['a' as const]);
88+
const result = Map({ a: 4, b: true }).getIn(['a']);
8989

9090
expect(result).type.toBeNumber();
9191

@@ -100,9 +100,9 @@ test('#getIn', () => {
100100
])
101101
).type.toBeNumber();
102102

103-
// currently `RetrievePathReducer` does not work with anything else than `MapOf`
104-
// TODO : fix this with a better type, it should be resolved to `number` (and not be marked as `fail`)
105-
expect.fail(Map({ a: List([1]) }).getIn(['a' as const, 0])).type.toBeNumber();
103+
expect(Map({ a: [1] }).getIn(['a' as const, 0])).type.toBeNumber();
104+
105+
expect(Map({ a: List([1]) }).getIn(['a' as const, 0])).type.toBeNumber();
106106
});
107107

108108
test('#set', () => {

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.