Improve Typescript definition for Map#1841
Conversation
e5ee08e to
0d49ee8
Compare
d8e332d to
b5d874a
Compare
|
Working today on seeing if |
|
@jdeniau I spent a lot of time on Mind if I have write access to your fork so I can push my changes? |
|
Happy to work more on this with you as well, if you have a good idea of what else needs implementing in order to review & merge. |
8cf0983 to
fe8dd03
Compare
fe8dd03 to
c762557
Compare
|
@mbullington I gave you access on my fork. You can push on the jd-feat-tsMapObject branch. If you want to continue on the subject, there are still some issues with constructor like |
… into jd-feat-tsMapObject
|
Was able to push my changes. Thinking about What's the issue with |
|
Nice implementation ! One think I'm not sure about (and something I did added) is the About |
|
@bdurrer I will open a discussion or a wiki page once this is merged to keep the CHANGELOG light |
|
Thanks all and @jdeniau for working on this! I can understand the argument that I think the main goal of this is to enable strong typing for the "immutable POJOs," which in a lot of cases due to Right now it's hard to justify using the Immutable typing with For people like @bdurrer 's use case I'd hate to flip this around, trading one alienated use case for another. If we can strike a balance (which I hope we've done?) both groups will be able to work productively. I've been playing around with an internal version for Mapbox Studio that recursively types I'd love some feedback on if these should be added or if the from/toJS logic is even correct: https://gist.github.com/mbullington/631dd21f910e594b29fb97dd6f3e62e8 |
4674d2d to
d627ad3
Compare
| // https://github.com/immutable-js/immutable-js/issues/1462#issuecomment-584123268 | ||
|
|
||
| /** @ignore */ | ||
| type GetMapType<S> = S extends MapFromObject<infer T> ? T : S; |
There was a problem hiding this comment.
This implies that getIn will be used on something that is MapFromObject all the way down. What happens when it includes a typical Map or List?
There was a problem hiding this comment.
I think that it will just return the type Map or List, am I correct? @jdeniau
There was a problem hiding this comment.
On something that is not a MapFromObject, it will return the type directly. I do not really know in the end what will happen though. Maybe @mbullington who coded this part ?
I thing that:
MapFromObject<{ m: Map<string, number>; l: List<number>; }>
GetMapType<m['m']> // Map<string, number>
GetMapType<m['l']> // List<number>Reading this particular line, the function only extract the initial object of a MapFromObject, that's all.
So in a chain:
MapFromObject<{
m: Map<string, List<number>>
}>Calling RetrievePath<R, ['m', 'some-string', number]> should return number
| getIn<P extends ReadonlyArray<string | number | symbol>>( | ||
| searchKeyPath: [...P], | ||
| notSetValue?: unknown | ||
| ): RetrievePath<R, P>; |
There was a problem hiding this comment.
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
| get<NSV>(key: string, notSetValue: NSV): NSV; | ||
|
|
||
| // https://github.com/microsoft/TypeScript/pull/39094 | ||
| getIn<P extends ReadonlyArray<string | number | symbol>>( |
There was a problem hiding this comment.
A nested structure may have keys that are not string | number | symbol
I think this needs to be ReadonlyArray<any> to account for all possible key types of a typical Map
There was a problem hiding this comment.
I do not know why, but if I do that, calling getIn(['a', 'b', 'c', 'd']) is not handled and fallback to be a never type
| notSetValue?: unknown | ||
| ): RetrievePath<R, P>; | ||
|
|
||
| set<K extends keyof R>(key: K, value: R[K]): this; |
There was a problem hiding this comment.
similar to get above, I think this needs another definition which covers where key is not in keyof R with an appropriate return type
There was a problem hiding this comment.
if key is not in keyof R immutable will return a undefined right?
There was a problem hiding this comment.
@EduardoAraujoB if key is not in keyof R, then technically a new Map is returned:
Map({}).set('a', 'a'); // output Map({ a: 'a' })@leebyron As said on slack I prefered the solution of replicating TypeScript objects: you need to define the interface before:
const o = { a: 'a' };
o.b = 'b'; // Property 'b' does not exist on type '{ a: string; }'.| * 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; |
There was a problem hiding this comment.
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
| // @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'); |
There was a problem hiding this comment.
I'm confused - is this not currently failing? Why does this PR cause this to start producing errors?
| // $ExpectError | ||
| Map({ a: 4 }).get('b'); |
There was a problem hiding this comment.
Can you add .get('b', undefined) to test the alternate NSV definition?
|
|
||
| // $ExpectType number | ||
| Map({ a: Map({ b: Map({ c: Map({ d: 4 }) }) }) }).getIn(['a', 'b', 'c', 'd']); | ||
| } |
There was a problem hiding this comment.
Can you add getIn tests where nested Map are constructed by other means (eg not only ObjectFromMap type?) as well as having List within?
There was a problem hiding this comment.
Unfortunatly, it's only working for MapFromObject for now.
| Map<number, number | string>().set(0, 'a'); | ||
|
|
||
| // $ExpectError | ||
| Map({ a: 1 }).set('b', 'b'); |
There was a problem hiding this comment.
I'm surprised by this, I'd expect either MapFromObject<{ a: 1, b: 'b' }> or Map<string, number | string> as a result?
There was a problem hiding this comment.
See comment above about the consistency with plain objects in TS
|
I took a stab at making sure @leebyron Thanks for looking and identifying these issues with In honesty I changing My reasoning is we're dealing with both string literal types like This could present issues with typing I wonder if there's a better way of typing |
|
Hi @mbullington You can get the type of the given key path of a plain JS object like type Arr = readonly unknown[];
type GetInType<T, KeyPath extends Arr> =
KeyPath extends [infer U]
? U extends keyof T
? T[U]
: 'fail 1'
: KeyPath extends [infer K, ...infer Rest]
? K extends keyof T
? GetInType<T[K], Rest>
: 'fail 2'
: 'fail 3'
// ---
const obj = {
foo: {
list: [1,2,3],
listObj: [{name: "alex"},{name: "sam"}],
},
num: 123,
};
type test = GetInType<typeof obj, ['foo', 'list']>;So, the type of For ImmutableJS, I guess we could replace |
faee487 to
beeac64
Compare
|
What's the status on this? Looks like it would be highly beneficial for a project my team is working on |
|
@jbaca303 it's a work in progress but it's really complex to make this work properly. Don't expect it to be done in the next few weeks, but if you would like to try it on your project, every return would be appreciable. |
Thanks for the quick update. Totally understand that this complex. Thank you guys for working on this, really appreciate it! |
|
I am using this for nearly 2 years now at https://github.com/mapado internally. I think it is time to go public now. I will release a RC on a 5.0 version to have some community return on this. Thanks you all for your patience. |
Imagine the following code:
This was previously typed as
Map<string, string | number>and return type of
m.get('length')orm.get('inexistant')was typed asstring | number | undefined.This made
Mapreally unusable with TypeScript.Now the Map is typed like this:
and the return type of
m.get('length')is typed asnumber.The return of
m.get('inexistant')throw the TypeScript error:If you want to keep the old definition
This is a minor BC for TS users, so if you want to keep the old definition, you can declare you Map like this:
If you need to type the Map with a larger definition
You might want to declare a wider definition, you can type your Map like this:
Are all
Mapmethods implemented ?For now, only
get,setandgetInmethods are implemented. All other methods will fallback to the basicMapdefinition. Other method definition will be added later, but as some might be really complex, we prefer the progressive enhancement on the most used functions.