Home > OS >  Typescript map union of objects to union of objects
Typescript map union of objects to union of objects

Time:07-15

I wrote some typescript code and I am not quiet sure why it is not working as I expected. I would really appriopriate if someone would explain me what I do wrong or what other approach should I use.

Let's say I have a Definition of a function name and data.

interface Definition<TName extends string, TData extends any> {
  name: TName;
  data: TData;
}

Definition at some point changes into Action. Action has a function named method with payload as first paramater type.

interface Action<TDefinition extends Definition<string, any>> {
  name: TDefinition["name"];
  method: (data: TDefinition["data"]) => void;
}

Keep in mind that all Definition union elements must move through Changer.

interface Changer<TDefinition extends Definition<string, any>> {
  actions: Action<TDefinition>[];
}
interface Definition<TName extends string, TData extends any> {
  name: TName;
  data: TData;
}

interface Action<TDefinition extends Definition<string, any>> {
  name: TDefinition["name"];
  method: (data: TDefinition["data"]) => void;
}

interface Changer<TDefinition extends Definition<string, any>> {
  actions: Action<TDefinition>[];
}

const set: Changer<Definition<"one", 1> | Definition<"two", 2>> = {
  actions: [
    {
      name: "one",
      method: (data) => data, // data type is equal to '1 | 2' I would expect it to be '1'
    },
  ],
};

set.actions[0].method(1); // data type is equal to '1 | 2' I would expect it to be '1'

Could you tell me what should I do to achieve result I expect?

I am thinking new Definition, however I belive I may have some issues because of [key: string]: any.

interface Definition<TName extends string, TData extends { [key: string]: any }> {
  [key in TName]: TData[key]
}

What do you think?

CodePudding user response:

This can be solved by using distributive conditional types.

interface Changer<TDefinition extends Definition<string, any>> {
  actions: (
    TDefinition extends infer U extends Definition<string, any> 
      ? Action<U> 
      : never
  )[];
}

We basically want to change the type of actions from

actions: Action<Definition<"one", 1> | Definition<"two", 2>>[]

to the type

actions: (Action<Definition<"one", 1>> | Action<Definition<"two", 2>>)[]

This can be done by forcing the union elements of TDefinition to be distributed by a conditional to individual Actions.

Playground


Now we have the correct type for the parameters in the functions.

const set: Changer<Definition<"one", 1> | Definition<"two", 2>> = {
  actions: [
    {
      name: "one",
      method: (data) => data, // data: 1
    },
    {
      name: "two",
      method: (data) => data // data: 2
    }
  ],
};

But this statement won't work:

set.actions[0].method(1); // Argument of type 'number' is not assignable to parameter of type 'never'

The set variable simply does not have information about individual array elements because you gave it an explicit type. To solve this you could create the set with a generic function.

  • Related