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

Better Type Inference #1812

Open
EduardoAraujoB opened this issue May 14, 2021 · 17 comments · Fixed by #1841
Open

Better Type Inference #1812

EduardoAraujoB opened this issue May 14, 2021 · 17 comments · Fixed by #1841

Comments

@EduardoAraujoB
Copy link

EduardoAraujoB commented May 14, 2021

Problem

So, let's suppose that you created a variable in this way const user = fromJS({ profile: { name: 'username' } });, so the main problem in that this variable has a type any which makes harder to find what this variable really is, one example is if someone just make a shadowing in that constant using the toJS() method, that will cause a lot of errors and things will get harder to know what type it has.
There is also another problem, which is to infer the types of subarrays and subobjects (like the name inside the profile), which will requires recursive type, something reeaaly weird to do

My local fix

Note: this solution is a huge hack and could cause troubles in the type system, keep that in mind

So to infer the types in a recursive way for objects (keeping the same type for all the rest), here is a huge hack that I made and fixed the type inference problem for me

interface ImmutableMap<T> extends Map<string, any> {
  get<K extends keyof T>(
    name: K,
  ): T[K] extends object
    ? T[K] extends any[]
      ? List<T[K]>
      : ImmutableMap<T[K]>
    : T[K];
  getIn<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]>(
    path: [K1, K2, K3],
  ): T[K1][K2][K3] extends object
    ? T[K1][K2][K3] extends any[]
      ? List<T[K1][K2][K3]>
      : ImmutableMap<T[K1][K2][K3]>
    : T[K1][K2][K3];
  getIn<K1 extends keyof T, K2 extends keyof T[K1]>(
    path: [K1, K2],
  ): T[K1][K2] extends object
    ? T[K1][K2] extends any[]
      ? List<T[K1][K2]>
      : ImmutableMap<T[K1][K2]>
    : T[K1][K2];
  getIn<K1 extends keyof T>(
    path: [K1],
  ): T[K1] extends object
    ? T[K1] extends any[]
      ? List<T[K1]>
      : ImmutableMap<T[K1]>
    : T[K1];
  toJS(): T;
  equals(obj: any): boolean;
}

this solution doesn't not cover everything, but fixed my problem and if something is missing, I can just add it

@cypherfunc
Copy link

I like the idea; I think this could be cleaner and more powerful if it made use of the TS 4.x improvements to Tuples, variadic types, and generic types. Been meaning to take a crack at doing this to Map for a while; I prefer HM-style types, so recursion and type-algebraic shenanigans feel a little more natural. 🤷

@EduardoAraujoB
Copy link
Author

yeah, this solution worked well and fixed my problems for type inference, but there is problems in this implementation, the first one is that the getIn type inference only works with a maximum of 4 properties and to make it work for more we need to keep repeating getIn function with more arguments, other problem is that this implementation doesn't implement the types for all the functions that Map provides, and the last one is more complicated, sometimes it's hard to know if something is in a Immutable way or not, for an example, Records doesn't apply deep conversion, so the first object is in a Immutable way and the second one isn't, but when we use the fromJS it does, so know when something is immutable or not is complicated

@jdeniau
Copy link
Member

jdeniau commented Jun 30, 2021

I think there is a huge problem with the actual Map interface.
The actual definition of the interface is :

interface Map<K, V> extends Collection.Keyed<K, V>

That is blocking a lot of things as it dissociate the keys and the values. It is useful as a Map is a Collection.Keyed<K, V>, and it does make sense with the internal implementation, but it is not flexible enough for the real use cases.

I started working on a different implementation for Map that are objects that might address these issues (still work in progress)

  interface ObjectLikeMap<R extends {[key in string|number]: unknown}, K extends keyof R, V extends R[K]> extends Map<K, V> {
    get(index: K, notSetValue: never): R[K];
    get(index: K): R[K];
  }

@EduardoAraujoB
Copy link
Author

EduardoAraujoB commented Jun 30, 2021

@jdeniau so, there is a problem on your implementation, as I said, it will not work for deep nested objects, for and example, if for some reason you have

const user = { name: 'name', profile: { role: 'user' } };

and a interface like this

interface User {
  name: string;
  profile: { role: string };
}

so what will happens when you put it into a fromJS?
it will deep convert all your objects into a Map and arrays as List, but the problem is that it will not infer the profile is a Map also, so we will need to change all our interfaces hardcoding types because it's not inferred properly (or keeping everything as any), and there is a huge problem if was not us that made that interface, we will need to override the types of other libraries or re-create it by hand which is bad, that's why I'm using that ImmutableMap type to infer everything properly and it's working good so far

@jdeniau
Copy link
Member

jdeniau commented Jun 30, 2021

@EduardoAraujoB Yes, fromJS is from far the hardest part ! 🤯

@jdeniau
Copy link
Member

jdeniau commented Jul 19, 2021

We are working on Map in #1841
fromJS upgrade won't probably be included in 4.0 though, as there is a lot to do with Map before.
If you are good in TypeScript and if you want to contribute, every help would be great ! 😃

@jdeniau jdeniau linked a pull request Jul 19, 2021 that will close this issue
@EduardoAraujoB
Copy link
Author

thanks a lot @jdeniau I will check it out the progress and see if I can help

@antonkulaga
Copy link

Immutable js types have many flows (I mentioned one of them at #1884 ). Overall using immutable.js in typescript is a nightmare because of constant type issues that are not always consistent between nodejs and browser based typescripts! I am considering moving away from it unless types will be improved.

@antonkulaga
Copy link

@jdeniau do you have progress at your Map implementation? The fast that I cannot convert { [p: string]: number }[] to Immutable.Map or Immutable.OrderedMap (opened an issue #1885 ) is very annoying

@jdeniau
Copy link
Member

jdeniau commented Oct 24, 2021

@antonkulaga you can just read the last two comments and you may find an open PR that tries to solve some issues with Map in TS...

You already opened two issues about TS. No need to be pushy with two comments on another issue mentioning yours.

immutable-js is open source work. All the maintainers take on their free time to work on this (for example it's Sunday morning and I'm replying to you).
This is typically this kind of comportement that makes open source maintainer having burnout.

About the "threat" about not using immutable anymore: we don't owe you anything, so if you are not helping or enjoying immutable, please do use another alternative.

If you want to help though, you can :

  • makes your issues the more easy to debug for us by at least post useful example,
  • accept the fact that issues will be addressed whenever possible, and find an alternative in the meantime,
  • get your hands in the code and open PRs

Thank you

@Isaac-Leonard
Copy link

Personally I found immutable unusable due to the inability to narrow unions of Map types but I think this is more a limitation of both typescript and javascript art this time.
I wanted to have a type like:

type Field = "String"|"Number"|{List:Field}|{Choice:Set<Field>}

```But converting this to immutible required the use of Map which couldn't be effectively discriminated against for the List and Choice variants.
This type was coming from auto generated API schemas so couldn't really be changed unfortunately.
If anyone knows how to do this without a bunch of flaky and case specific guard types I'm happy to implement it.
Proxies could maybe do it but given they're not being used already I assume the project has good reason to avoid them?

@jdeniau
Copy link
Member

jdeniau commented Nov 18, 2023

@Isaac-Leonard can you try the v5 RC? A lot of change has been made to the Map typescript.

@Isaac-Leonard
Copy link

Running 6pm install immutable gives me the v5 beta.4, I assume that's the most recent release?
My issue is shown in the code below:

export type Field =
  | "String"
  | "Number"
  | { List: Field }
  | { Choice: Set<Field> };

type P = FromJS<Field>;
let x: P = "String" as P;
if (typeof x !== "string") {
  x.has();
}

The call to .has doesn't work as it is the argument is typed as Never because of the union of the two map types.
I could write hacky type guards to get around that but would rather not.
Do you have any solutions?

@Isaac-Leonard
Copy link

Another issue is that the get method can return undefined even if the original objects properties are not typed as undefined.

@jdeniau
Copy link
Member

jdeniau commented Nov 19, 2023

Running 6pm install immutable gives me the v5 beta.4, I assume that's the most recent release?
My issue is shown in the code below:

export type Field =
  | "String"
  | "Number"
  | { List: Field }
  | { Choice: Set<Field> };

type P = FromJS<Field>;
let x: P = "String" as P;
if (typeof x !== "string") {
  x.has();
}

The call to .has doesn't work as it is the argument is typed as Never because of the union of the two map types.
I could write hacky type guards to get around that but would rather not.
Do you have any solutions?

I think that the FromJS of a union lost TS here. Immutable 5 latest change improve the Map type and introduced MapOf type.

I see you do export this type but I think I would have done this :

type P =
   | "String"
   | "Number"
   | MapOf<{ List: Field }>
   | MapOf<{ Choice: Set<Field> }>;

This way your if should know that this a Map or a Map and that has should work.

@Isaac-Leonard
Copy link

I can't seem to make it work properly.

type ImmutableField =
  | "String"
  | "Number"
  | MapOf<{ List: ImmutableField }>
  | { Choice: Set<ImmutableField> };

Works fine but assigning a value from fromJs() to a variable of type Immutable field doesn't work due to the differences between MapOf and Map.

I also can't get any methods to work on it:

const test2 = "String" as ImmutableField;
if (typeof test2 !== "string") {
  test2.has("List");
}

Just gives a error about has not being a field on test2 and it seems as though test2 has no fields or methods.

@jdeniau
Copy link
Member

jdeniau commented Nov 19, 2023

@Isaac-Leonard it's because you did not type { Choice: Set<ImmutableField> } as an immutable type here.

See the error message :
image

The if does work fine and exclude "String" and "Number" of your union, it does keep MapOf<{ List: ImmutableField }> and { Choice: Set<ImmutableField> } (a simple JS object). MapOf does contain has, but not a javascript object.

But you are right, pushing this forward (ts playgroud link), there is a issue of the union of MapOf because it does try to find a valid intersection between the keys "List" and "Choices" here 😒

I'm keeping that in mind if I have a fix in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants