-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Type safe getIn/setIn (TypeScript) #1462
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
Comments
Hi!
About 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 |
Hi,
What do you mean? It looks like setIn takes a value of type |
You are right about |
cc @DanielRosenwasser :) |
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; |
Ah, ok thanks! |
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. |
I was about to post an issue for improved 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
} |
@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 |
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 |
Give https://github.com/mweststrate/immer a try, which is much more type-friendly |
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. |
@cmcaine We have similar issue with our project, and currently we had refactored |
Thanks, that doesn't work so nicely for
Could you expand on this? It sounds interesting. |
@cmcaine Then you copy Structural sharing is a property of persistent data structures, provided by Please pay attention that not every data structure provided by 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. 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! |
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 that 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
}
} |
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;
}
}
|
I dont know the exact details of the library but this is possible without hardcoding everything |
@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 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>' |
any updates? |
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) |
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
orsetIn
.const r = nested.getIn(['some', 'long', 'path']) // r: any :'(
Any TS pros out there, who could help with the TS declarations?
The text was updated successfully, but these errors were encountered: