TypeScript | Immutable | proper way of extending Immutable.Map type
Asked Answered
A

3

7

I have a react-redux application written in typescript with immutable package. There I have a data, which comes from api and in store I pack it to Map. In all application they are used as a Map.

I created an interface:

export interface PaymentMethod extends Immutable.Map<string, string | NamedType<number>> {
    id: string;
    name: string;
    description: string;
    accountNr: string;
    paymentMethodType: NamedType<number>;
}

In general it works very good. Except tests, where I create data this way:

const dummyPaymentMethod: PaymentMethod = Map({
    id: '',
    name: '',
    description: '',
    accountNr: '',
    paymentMethodType: { id: 1, name: '' },
});

and then I get a lint error:

Error:(116, 13) TS2322:Type 'Map<string, string | { id: number; name: string; }>' is not assignable to type 'PaymentMethod'.
Property 'id' is missing in type 'Map<string, string | { id: number; name: string; }>'.

I feel totally lost, as I can see id in interface and in my dummy data.

I would appreciate some light on it. I feel that somehow I should pass the list of acceptable keys to my Map, but no idea, how to do this.

EDIT: misspell

Amusement answered 25/4, 2017 at 10:7 Comment(0)
R
16

We used it like this in our project (slightly different approach):

interface ImmutableMap<T> extends Map<string, any> {
  get<K extends keyof T>(name: K): T[K];
}

We used an older version of the Immutable.js typings that didn't used mapped types yet (T[K]). AFAIK typings are updated since then and there is no need to overwrite the get method.

EDIT: Actually the get method still is not fully type safe unlike the above. So overwriting the method still has its merits.

With the above declaration you can then create immutable maps like:

type AuthState = ImmutableMap<{
  user:string|null;
  loggedIn:boolean;
}>;

const authState:AuthState = fromJS({ user: 'Alice', loggedIn: true });

Ideally, you would like typings like this:

/**
 * Imaging these are typings for your favorite immutable
 * library. We used it to enhance typings of `immutable.js`
 * with the latest TypeScript features.
 */
declare function Immutable<T>(o: T): Immutable<T>;
interface Immutable<T> {
  get<K extends keyof T>(name: K): T[K];
  set<S>(o: S): Immutable<T & S>;
}

const alice = Immutable({ name: 'Alice', age: 29 });
alice.get('name');      // Ok, returns a `string`
alice.get('age');       // Ok, returns a `number`
alice.get('lastName');  // Error: Argument of type '"lastName"' is not assignable to parameter of type '"name" | "age"'.

const aliceSmith = alice.set({ lastName: 'Smith' });
aliceSmith.get('name');     // Ok, returns a `string`
aliceSmith.get('age');      // Ok, returns a `number`
aliceSmith.get('lastName'); // Ok, returns `string`

Link to the Playground


In order to achieve the above with Immutable.js you can create a small helper function, whose only purpose is to "fix" typings:

import { fromJS } from 'immutable';

interface Immutable<T> {
  get<K extends keyof T>(name: K): T[K];
  set<S>(o: S): Immutable<T & S>;
}

function createImmutable<T extends object> (o:T) {
  return fromJS(o) as Immutable<T>;
}

Note that I used fromJS in the example. This will create a Map as long as the passed input is an Object. The benefit of using fromJS over Map is that the typings are easier to overwrite.

Side note: You might also want to look into Records.

Rolfrolfe answered 25/4, 2017 at 11:15 Comment(2)
Thanks, I got very useful information from your answer.Amusement
Would this apply to Record (with just get) as well?Dg
E
1

Leaving that here in case it helps someone:

import { Map } from 'immutable';

export interface ImmutableMap<JsObject extends object> extends Omit<Map<keyof JsObject, JsObject>, 'set' | 'get' | 'delete'> {
    set: <AKeyOfThatJsObject extends keyof JsObject>(key: AKeyOfThatJsObject, value: ValueOf<Pick<JsObject, AKeyOfThatJsObject>>) => ImmuMap<JsObject>;
    get: <AKeyOfThatJsObject extends keyof JsObject>(key: AKeyOfThatJsObject) => ValueOf<Pick<JsObject, AKeyOfThatJsObject>>;
    delete: <AKeyOfThatJsObject extends keyof JsObject>(key: AKeyOfThatJsObject, value: ValueOf<Pick<JsObject, AKeyOfThatJsObject>>) => ImmuMap<JsObject>;
}

It's really working great for us!

We use it like so: const aMap: ImmutableMap<OneOfOurObjectTypes>

Example Usage:

type BarType = { bar: string };

const foo: BarType = {
    bar: 'abc';
};
const immutableBar: Immutable<BarType> = Map(foo);
// immutableBar.get suggests bar as the key, and knows that the result is a string.

Exobiology answered 18/12, 2020 at 22:54 Comment(1)
ValueOf is not defined. Same as ImmuMap<JsObject> but i expect it to be ImmutableMap<JsObject>. Maybe ValueOf is part of type-fest package? (yarn add type-fest)<Bolick
T
1

If you find yorself using immutable.js maps more and more like native objects, it could be that a library like immer is more suitable.

Otherwise, here's an example of a complete version, including filter etc:

import { fromJS, Map } from 'immutable'
type Modify<T, R> = Omit<T, keyof R> & R;
type AnyObject = Record<string, any>

export const TypedMap = <T extends AnyObject>(val: T) => 
fromJS(val) as any as TypedMap<T>

export type TypedMap<T extends AnyObject> = 
    Modify<Map<keyof T, T[keyof T]>, TypedMapBase<T>>

export interface TypedMapBase<T extends AnyObject>  {
    set: <K extends keyof T>(key: K, value: T[K]) => this
    get: <K extends keyof T>(key: K) => T[K]
    delete: <K extends keyof T>(key: K) => boolean
    deleteAll<K extends keyof T>(keys: Iterable<K>): this
    remove<K extends keyof T>(key: K): this
    removeAll<K extends keyof T>(keys: Iterable<K>): this
    update<K extends keyof T>(key: K, notSetValue: T[K], updater: (value: T[K]) => T[K]): this
    update<K extends keyof T>(key: K, updater: (value: T[K]) => T[K]): this
    merge<KC, VC, K extends keyof T>(
        ...collections: Array<Iterable<[KC, VC]>>
        ): Map<K | KC, T[K] | VC>
    merge<C, K extends keyof T>(
        ...collections: Array<{ [key: string]: C }>
    ): Map<K | string, T[K] | C>
    concat<KC, VC, K extends keyof T>(
        ...collections: Array<Iterable<[KC, VC]>>
    ): Map<K | KC, T[K] | VC>
    concat<C, K extends keyof T>(
        ...collections: Array<{ [key: string]: C }>
    ): Map<K | string, T[K] | C>
    mergeWith<K extends keyof T>(
        merger: <K extends keyof T>(oldVal: T[K], newVal: T[K], key: K) => T[K],
        ...collections: Array<Iterable<[K, T[K]]> | { [key: string]: T[K] }>
    ): this
    mergeDeep<K extends keyof T>(
        ...collections: Array<Iterable<[K, T[K]]> | { [key: string]: T[K] }>
    ): this
    mergeDeepWith<K extends keyof T>(
        merger: (oldVal: unknown, newVal: unknown, key: unknown) => unknown,
        ...collections: Array<Iterable<[K, T[K]]> | { [key: string]: T[K] }>
    ): this
    map<M, K extends keyof T>(
        mapper: (value: T[K], key: K, iter: this) => M,
        context?: unknown
    ): Map<K, M>
    mapKeys<M, K extends keyof T>(
        mapper: (key: K, value: T[K], iter: this) => M,
        context?: unknown
    ): Map<M, T[K]>
    mapEntries<KM, VM, K extends keyof T>(
        mapper: (
            entry: [K, T[K]],
            index: number,
            iter: this
        ) => [KM, VM],
        context?: unknown
    ): Map<KM, VM>
    flatMap<KM, VM, K extends keyof T>(
        mapper: (value: T[K], key: K, iter: this) => Iterable<[KM, VM]>,
        context?: unknown
    ): Map<KM, VM>
    filter<F extends T[K], K extends keyof T>(
        predicate: (value: T[K], key: K, iter: this) => value is F,
        context?: unknown
    ): Map<K, F>
    filter<K extends keyof T>(
        predicate: (value: T[K], key: K, iter: this) => unknown,
        context?: unknown
    ): this
    flip<K extends keyof T>(): Map<T[K], K>
}



And a partial extension:

export const PartialTypedMap = <T extends AnyObject>(val: Partial<T>) => 
    fromJS(val) as any as PartialTypedMap<T>


export type PartialTypedMap<T extends AnyObject> = Modify<TypedMap<T>, PartialTypedMapBase<T>>

interface PartialTypedMapBase<T extends AnyObject>{
    get: <K extends keyof T>(key: K) => T[K] | undefined
    update<K extends keyof T>(key: K, updater: (value: T[K]|undefined) => T[K]): this
    mapEntries<KM, VM, K extends keyof T>(
        mapper: (
            entry: [K, T[K]],
            index: number,
            iter: this
        ) => [KM, VM] | undefined,
        context?: unknown
    ): Map<KM, VM>
}

Thermometer answered 18/7, 2022 at 0:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.