1

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?

2
  • Object properties and map entries are not the same thing. Which one do you need? Commented Feb 22, 2017 at 10:34
  • @TamasHegedus indeed entries of the map Commented Feb 22, 2017 at 10:35

2 Answers 2

3

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.

Sign up to request clarification or add additional context in comments.

3 Comments

My god, static typing is getting out of hand! What will happen if somebody sets a key with a simple string type and not a literal type?
Oh I see, you cannot do that.
AFAIK currently TypeScript can not infer this correctly . But you can change the set signature and use a generic. I'll update my question.
2

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.

3 Comments

Good idea, but in this way I do not have typecheck on the value of set, because the generic set(key: K, value: V) makes any value acceptable for any key. Do you have an idea to disable that behavior?
Shouldn't be set(name: "b", value: any): void?
@rpadovani Right, my bad. Fixed the answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.