rpadovani
rpadovani

Reputation: 7360

Typescript and ImmutableJS.set()

I want to have an ImmutableJS Map which has to have always defined some properties:

interface MyImmutableMap extends Immutable.Map<string, any> {
  a: number;
  b: number;
  c?: string;
}

I have a function which edits the map:

function myFunction(myMap: MyImmutableMap, flag: boolean, value: number): MyImmutableMap {
  if (flag) {
     return myMap
       .set('a', value);
  }

  return myMap;
}

The second return is ok, because it returns the same thing I received, therefore an MyImmutableMap.

Unfortunately the set() doesn't work, because its return type is a Map and not the type of myMap.

This is the error:

Type 'Map' is not assignable to type 'MyImmutableMap'.
Property 'a' is missing in type 'Map'.

There is any way to go around this?

Upvotes: 1

Views: 2043

Answers (2)

Nitzan Tomer
Nitzan Tomer

Reputation: 164467

The following interface:

interface MyImmutableMap extends Immutable.Map<string, any> {
    a: number;
    b: number;
    c?: string;
}

Defines an instance of Immutable.Map which also has the a, b and c properties:

let m: MyImmutableMap;
m.a = 4;
m.b = 6;
m.c = "str";

You cannot control the map key/values like that.

You can do this:

type MyImmutableMap = Immutable.Map<string, any> & {
    set(name: "a", value: number);
    get(name: "a"): number;

    set(name: "b", value: number);
    get(name: "b"): number;

    set(name: "c", value: string);
    get(name: "c"): string | undefined;
}

Edit

You can "override" the default set like so:

type MyImmutableMap = Immutable.Map<string, any> & {
    set(name: "a", value: number);
    set(name: "a", value: any): void
    get(name: "a"): number;

    set(name: "b", value: number);
    set(name: "b", value: any): void
    get(name: "b"): number;

    set(name: "c", value: string);
    set(name: "c", value: any): void
    get(name: "c"): string | undefined;
}

Then:

return myMap
    .set('c', 45);
}

Results in:

Type 'void' is not assignable to type 'Map'

It's not the best error message for this case, it's still restricted.

Upvotes: 2

Sebastian Sebald
Sebastian Sebald

Reputation: 16906

If you only have primitive values, you can also use Records instead of a Map.

Sadly, the typings for immutable.js aren't that good and I am not sure if they are actively maintained by the authors (because the typings are part of the repo and not @types).

We had a similar issue and used type assertions to overwrite the Map with a "better" version:

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`

Since the set of immutable.js is something like set(key:string, value:any) you would need to use a generic to help the compiler:

declare function Immutable<T>(o: T): Immutable<T>;
interface Immutable<T> {
  get<K extends keyof T>(name: K): T[K];
  set<S>(key:string, value:any): 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':string }>('lastName', 'Smith');
aliceSmith.get('name');     // Ok, returns a `string`
aliceSmith.get('age');      // Ok, returns a `number`
aliceSmith.get('lastName'); // Ok, returns `string`

Here is a link to the playground.

Upvotes: 3

Related Questions