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

Type safe getIn/setIn (TypeScript) #1462

Open
dinony opened this issue Dec 19, 2017 · 21 comments
Open

Type safe getIn/setIn (TypeScript) #1462

dinony opened this issue Dec 19, 2017 · 21 comments

Comments

@dinony
Copy link

dinony commented Dec 19, 2017

I'm using v4.0.0-rc.9 with TypeScript a lot.
I wonder if it would be possible to preserve type information when using getIn or setIn.

const r = nested.getIn(['some', 'long', 'path']) // r: any :'(

Any TS pros out there, who could help with the TS declarations?

@remi-blaise
Copy link

Hi!

setIn actually preserve type information.

About getIn, returning type information would require calculating type from keyPath and type information of nested properties, which is unfortunately currently impossible in Typescript 2.6.

For now Typescript only allow very basic type calculations and do not allow usage of the function parameters to make those calculations (you have to work with specialised type parameter in order to make type genericity).

So for now, displaying the return type of getIn is impossible for what I know, I'm sorry. It was worth asking by the way. I think we can close this issue.

@dinony
Copy link
Author

dinony commented Jan 1, 2018

Hi,

setIn actually preserve type information.

What do you mean? It looks like setIn takes a value of type any as second argument. I thought similar type calculations would help also with setIn, but I'm unfortunately not that familiar with the TS spec.
Thanks for your feedback that such type calculations are not possible.

@remi-blaise
Copy link

You are right about setIn, I didn't think you was speaking not about the return type but the type of the second argument. It seems indeed to be the same type of calculation than getIn, but it seems even more complicated to me, I have no idea if it's possible to enforce an argument type based on a calculation (I suppose it's possible but I'm not even sure ^^).

@dinony
Copy link
Author

dinony commented Jan 1, 2018

cc @DanielRosenwasser :)

@DanielRosenwasser
Copy link

DanielRosenwasser commented Jan 2, 2018

You can get pretty close to the cases most people care about with a few overloads.

interface Cursor<T> {
    getIn<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]>(path: [K1, K2, K3]): T[K1][K2][K3];
    getIn<K1 extends keyof T, K2 extends keyof T[K1]>(path: [K1, K2]): T[K1][K2];
    getIn<K1 extends keyof T>(path: [K1]): T[K1];
    getIn(keyPath: any[], notSetValue?: any): any;
}

interface Foo {
    foo: {
        bar: {
            baz: {
                yay: number
            }
        }
    }
}

declare var x: Cursor<Foo>;

let a = x.getIn(["foo"]).bar;
let b = x.getIn(["foo", "bar"]).baz;
let c = x.getIn(["foo", "bar", "baz"]).yay;

@dinony
Copy link
Author

dinony commented Jan 2, 2018

Ah, ok thanks!
I also think that using this overloading pattern you can cover a lot of cases. Also, ngrx uses this technique too.

@DanielRosenwasser
Copy link

No problemo! Unfortunately it means that Cursor would need to become generic. You could change it to something like

interface Cursor<T = any> {
    // ...
}

to preserve the current behavior, but I'm not an ImmutableJS expert.

@inakianduaga
Copy link

I was about to post an issue for improved setIn types and I just saw this issue. This is what I have so far for a typesafe setIn

function setIn<T extends Record<any>, K1 extends keyof T, V extends T[K1]>(state: T, keys: [K1], v: V): T
function setIn<T extends object, K1 extends keyof T, K2 extends keyof T[K1], V extends T[K1][K2]>(
  state: T,
  keys: [K1, K2],
  v: V
): T
function setIn<
  T extends Record<any>,
  K1 extends keyof T,
  K2 extends keyof T[K1],
  K3 extends keyof T[K1][K2],
  V extends T[K1][K2][K3]
>(state: T, keys: [K1, K2, K3], v: V): T
function setIn<
  T extends Record<any>,
  K1 extends keyof T,
  K2 extends keyof T[K1],
  K3 extends keyof T[K1][K2],
  K4 extends keyof T[K1][K2][K3],
  V extends T[K1][K2][K3][K4]
>(state: T, keys: [K1, K2, K3, K4], v: V): T
function setIn<
  T extends Record<any>,
  K1 extends keyof T,
  K2 extends keyof T[K1],
  K3 extends keyof T[K1][K2],
  K4 extends keyof T[K1][K2][K3],
  V extends T[K1] | T[K1][K2] | T[K1][K2][K3] | T[K1][K2][K3][K4]
>(state: T, keys: Iterable<any>, v: V): T {
  return state.setIn(keys, v)
}

this would allow you (for a Record), to only be allowed to set nested properties that are present in the original object. For example

type TestRecord = {
  first: {
    second: {
      third: {
        foo: 'bar' | 'baz'
        real: boolean
      }
    }
  }
  array: boolean[]
}
type TestType = Record<TestRecord> & Readonly<TestRecord>

const t = (a: TestType) => {
  setIn(a, ['first', 'second', 'third', 'foo'], 'bar') //works
  setIn(a, ['first', 'second', 'third', 'real'], true) //works
  setIn(a, ['newKey', 2) //fails
  setIn(a, ['array'], [true, false]) //works
  setIn(a, ['array'], [true, false, 'foo']) //fails
}

@inakianduaga
Copy link

@leebyron Could it be possible to extend the Record Interface to support this:

export interface Record<TProps extends Object> {
  // ...

  /**
   * Replaces an existing key in `keyPath` with a type-compatible `value`
   * At run-time, this is an alias of `setIn`
   */
  replaceIn<K1 extends keyof TProps, V extends TProps[K1]>(keyPath: [K1], value: V): this;
  replaceIn<K1 extends keyof TProps, K2 extends keyof TProps[K1], V extends TProps[K1][K2]>(
    keyPath: [K1, K2],
    value: V
  ): this;
  replaceIn<
    K1 extends keyof TProps,
    K2 extends keyof TProps[K1],
    K3 extends keyof TProps[K1][K2],
    V extends TProps[K1][K2][K3]
  >(
    keyPath: [K1, K2, K3],
    value: V
  ): this;
  replaceIn<
    K1 extends keyof TProps,
    K2 extends keyof TProps[K1],
    K3 extends keyof TProps[K1][K2],
    K4 extends keyof TProps[K1][K2][K3],
    V extends TProps[K1][K2][K3][K4]
  >(
    keyPath: [K1, K2, K3, K4],
    value: V
  ): this;
  replaceIn<
    K1 extends keyof TProps,
    K2 extends keyof TProps[K1],
    K3 extends keyof TProps[K1][K2],
    K4 extends keyof TProps[K1][K2][K3],
    K5 extends keyof TProps[K1][K2][K3][K4],
    V extends TProps[K1][K2][K3][K4][K5]
  >(
    keyPath: [K1, K2, K3, K4, K5],
    value: V
  ): this;

  // ...
}

code overhead would be almost zero since replaceIn is an alias of setIn at runtime. This would be really useful for doing type-safe Redux updates in React where we don't want to set new properties or alter the type but do type-compatible updates on an existing data structure. Not sure if the type-definition would work out of the box for flow

@jeanfortheweb
Copy link

Same here, did almost the same locally as you guys. It's not that hard actually not implement that. But the result is just damn useful. +1 from me for this

@sunnylqm
Copy link

Give https://github.com/mweststrate/immer a try, which is much more type-friendly

@cmcaine
Copy link

cmcaine commented May 26, 2019

I couldn't work out how to monkey patch these definitions in and editing immutable/dist/immutable.d.ts didn't seem to help either.

I'd appreciate a complete example of how to get this working, if anyone is feeling generous.

@JustFly1984
Copy link

JustFly1984 commented May 27, 2019

@cmcaine We have similar issue with our project, and currently we had refactored getIn(["string1", "string2"]) to get("string1").get("string2")
The issues with refactoring to immer is:
1 - high effort and cost
2- immer doesn't provide persistence data structure features, which is crucial for performance in real-time applications

@cmcaine
Copy link

cmcaine commented May 28, 2019

Thanks, that doesn't work so nicely for setIn. I moved my code to immer, but the cost was low because I'm still at the prototyping phase.

immer doesn't provide persistence data structure features, which is crucial for performance in real-time applications

Could you expand on this? It sounds interesting.

@JustFly1984
Copy link

@cmcaine Then you copy JS object, you get memory usage doubled, and if you clear all references to old object, it will be picked up by Garbage Collector with all its properties.

Structural sharing is a property of persistent data structures, provided by immutable.js, where your new object and old objects share the memory for shared data. This architecture allows to minimize JS Garbage collector calls for browser and node.js engines by clearing the references to only actually removed properties of an immutable object.

Please pay attention that not every data structure provided by immutable.js has persistence properties. For example Record does not have persistence, meanwhile List and Map has.

Immutability is a concept, which provides you with single benefit: you know exactly where in your code an object has been changed, and forbid mutations, hence provide additional security to the data.

Persistence Data structures is another conception, happily coexisted with immutability in this library.
As I understand, only immutable data structures can have persistence features. but Immer has implemented immutability with a tradeoff - it trades persistence for better API

Hope there will be some evolution,which will add persistence that structures to native JavaScript and TypeScript in the future. If there is a proposal to EC39, tell me where to sign!

@johnjeng
Copy link

johnjeng commented Sep 18, 2019

In case anyone reading this needs propagated maybes like

interface Foo {
    foo: {
        maybe_bar?: {
            baz: {
                yay?: number
            }
        }
    }
}

const r: Record<Foo> = ...
// Should be of type:  { yay?: number } | undefined
r.getIn(["foo", "maybe_bar", "baz"]) 

With the type examples in previous comments, I found thatr.getIn(["foo", "maybe_bar", "baz"]), would result in "baz" is not assignable to never.

I wrote the typing below to get around that. Here's a playground link. It's kind of long and I think Higher Kinded Types or something would help (I'm no expert on type systems).

Hope this is helpful to someone!

type Maybe<T> = T | undefined;
type CopyMaybe<T, U> = Maybe<T> extends T ? Maybe<U> : U;
type CopyAnyMaybe<T, U, V> = CopyMaybe<T, V> | CopyMaybe<U, V>;

class ImmutableRecord<State extends object> {
  getIn<K1 extends keyof State>(path: [K1]): State[K1];
  getIn<K1 extends keyof State, K2 extends keyof NonNullable<State[K1]>>(
    path: [K1, K2]
  ): CopyMaybe<State[K1], NonNullable<State[K1]>[K2]>;
  getIn<
    K1 extends keyof State,
    K2 extends keyof NonNullable<State[K1]>,
    K3 extends keyof NonNullable<NonNullable<State[K1]>[K2]>
  >(
    path: [K1, K2, K3]
  ): CopyAnyMaybe<
    State[K1],
    NonNullable<State[K1]>[K2],
    NonNullable<NonNullable<State[K1]>[K2]>[K3]
  >;
  getIn<K1 extends keyof State, NSV>(    // Same stuff but with notSetValue. 
    path: [K1],
    notSetValue: NSV
  ): State[K1] | NSV;
  getIn<K1 extends keyof State, K2 extends keyof NonNullable<State[K1]>, NSV>(
    path: [K1, K2],
    notSetValue: NSV
  ): CopyMaybe<State[K1], NonNullable<State[K1]>[K2]> | NSV;
  getIn<
    K1 extends keyof State,
    K2 extends keyof NonNullable<State[K1]>,
    K3 extends keyof NonNullable<NonNullable<State[K1]>[K2]>,
    NSV
  >(
    path: [K1, K2, K3],
    notSetValue: NSV
  ): CopyAnyMaybe<
    State[K1],
    NonNullable<State[K1]>[K2],
    NonNullable<NonNullable<State[K1]>[K2]>[K3] | NSV
  >;
  getIn(path: any[], notSetValue?: any): any {
    return "" as any; // Implementation not important
  }
}

@eretica
Copy link

eretica commented Jan 27, 2020

I wrote the type definition of Record. Hope it helps.

import * as Immutable from 'immutable';

type Purify<T extends string> = { [P in T]: P }[T];
type Required<T> = T extends object
  ? { [P in Purify<keyof T>]: NonNullable<T[P]> }
  : T;

type extractGeneric<
  Type extends Immutable.Map<string, any> | any
> = Type extends Immutable.Map<infer K, infer V> ? { [P in K]: V } : Type;

type Nest1<TProps> = TProps;
type Nest2<TProps, K1> = extractGeneric<Required<TProps>[K1]>;
type Nest3<TProps, K1, K2> = extractGeneric<
  Required<extractGeneric<Required<TProps>[K1]>>[K2]
>;
type Nest4<TProps, K1, K2, K3> = extractGeneric<
  Required<
    extractGeneric<Required<extractGeneric<Required<TProps>[K1]>>[K2]>
  >[K3]
>;

declare module 'immutable' {
  export function Map<V extends {}>(obj: {
    [key: keyof V]: V[keyof V];
  }): V extends any ? Map<any, any> : Map<keyof V, V[keyof V]>;

  export interface Collection<K, V> extends Immutable.ValueObject {}

  export interface List<T> extends Immutable.Collection.Indexed<T> {}

  export interface Record<TProps extends Object> {
    getIn<
      K1 extends keyof Nest1<TProps>,
      R extends Nest1<TProps>[K1],
      N extends any
    >(
      path: [K1],
      notSetValue?: N
    ): undefined extends N ? R : N | R;
    getIn<
      K1 extends keyof Nest1<TProps>,
      K2 extends keyof Nest2<TProps, K1>,
      R extends Nest2<TProps, K1>[K2],
      N extends any
    >(
      path: [K1, K2],
      notSetValue?: N
    ): undefined extends N ? R : R | N;
    getIn<
      K1 extends keyof Nest1<TProps>,
      K2 extends keyof Nest2<TProps, K1>,
      K3 extends keyof Nest3<TProps, K1, K2>,
      R extends Nest3<TProps, K1, K2>[K3],
      N extends any
    >(
      path: [K1, K2, K3],
      notSetValue?: N
    ): undefined extends N ? R : R | N;
    getIn<
      K1 extends keyof Nest1<TProps>,
      K2 extends keyof Nest2<TProps, K1>,
      K3 extends keyof Nest3<TProps, K1, K2>,
      K4 extends keyof Nest4<TProps, K1, K2, K3>,
      R extends Nest4<TProps, K1, K2, K3>[K4],
      N extends any
    >(
      path: [K1, K2, K3, K4],
      notSetValue?: N
    ): undefined extends N ? R : R | N;

    mergeIn<
      K1 extends keyof Nest1<TProps>,
      M extends Partial<NonNullable<Nest1<TProps>[K1]>>
    >(
      path: [K1],
      object: M
    ): this;
    mergeIn<
      K1 extends keyof Nest1<TProps>,
      K2 extends keyof Nest2<TProps, K1>,
      M extends Partial<NonNullable<Nest2<TProps, K1>[K2]>>
    >(
      path: [K1, K2],
      object: M
    ): this;
    mergeIn<
      K1 extends keyof Nest1<TProps>,
      K2 extends keyof Nest2<TProps, K1>,
      K3 extends keyof Nest3<TProps, K1, K2>,
      M extends Partial<NonNullable<Nest3<TProps, K1, K2>[K3]>>
    >(
      path: [K1, K2, K3],
      object: M
    ): this;
    mergeIn<
      K1 extends keyof Nest1<TProps>,
      K2 extends keyof Nest2<TProps, K1>,
      K3 extends keyof Nest3<TProps, K1, K2>,
      K4 extends keyof Nest4<TProps, K1, K2, K3>,
      M extends Partial<NonNullable<Nest4<TProps, K1, K2, K3>[K4]>>
    >(
      path: [K1, K2, K3, K4],
      object: M
    ): this;

    updateIn<
      K1 extends keyof Nest1<TProps>,
      M extends NonNullable<Nest1<TProps>[K1]>
    >(
      path: [K1],
      updater: (value: M) => NonNullable<Nest1<TProps>[K1]>
    ): M;
    updateIn<
      K1 extends keyof Nest1<TProps>,
      K2 extends keyof Nest2<TProps, K1>,
      M extends NonNullable<Nest2<TProps, K1>[K2]>
    >(
      path: [K1, K2],
      updater: (value: M) => M
    ): this;
    updateIn<
      K1 extends keyof Nest1<TProps>,
      K2 extends keyof Nest2<TProps, K1>,
      K3 extends keyof Nest3<TProps, K1, K2>,
      M extends NonNullable<Nest3<TProps, K1, K2>[K3]>
    >(
      path: [K1, K2, K3],
      updater: (value: M) => M
    ): this;
    updateIn<
      K1 extends keyof Nest1<TProps>,
      K2 extends keyof Nest2<TProps, K1>,
      K3 extends keyof Nest3<TProps, K1, K2>,
      K4 extends keyof Nest4<TProps, K1, K2, K3>,
      M extends NonNullable<Nest4<TProps, K1, K2, K3>[K4]>
    >(
      path: [K1, K2, K3, K4],
      updater: (value: M) => M
    ): this;
  }
}

update In merge In will not work unless you comment out the original d.ts

// updateIn(keyPath: Iterable<any>, updater: (value: any) => any): this;
// mergeIn(keyPath: Iterable<any>, ...collections: Array<any>): this;

@Mateiadrielrafael
Copy link

I dont know the exact details of the library but this is possible without hardcoding everything

@witalobenicio
Copy link

@eretica by using your approach, how should I define Map when using?

Let me say I have an object to represent my state like this:

interface StateProps<T> {
   type: string;
   loading: boolean;
   payload: T;
}

When defining Map how should I do?

Right now I tried following way:

type StateTypes<T> = StateProps<T>[keyof StateProps<T>];
type StateKeys<T> = [keyof StateProps<T>];

Map<StateKeys<StateProps<T>>, StateTypes<T>>

but when I try to get with:

prepareReducer.getIn(['payload'])

I get an error:

Argument of type 'string | boolean | IPrepare' is not assignable to parameter of type 'SetStateAction<IPrepare | undefined>'.   Type 'string' is not assignable to type 'SetStateAction<IPrepare | undefined>'

@long76
Copy link

long76 commented Sep 28, 2022

any updates?

@jdeniau
Copy link
Member

jdeniau commented Sep 28, 2022

The most advanced option is #1841, but I do not have time to push it now and technically it will be a breaking change for TS users, so I io not want to push it if it's not ready (at least I don't want to break two times types)

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

No branches or pull requests