Home > Mobile >  Union of interfaces with a type field, create an object typed as: type - specific attribute of that
Union of interfaces with a type field, create an object typed as: type - specific attribute of that

Time:11-05

This question is very hard to explain with words, and easy with code :-) Here's what I'm looking to achieve (TS Playground)

enum ActionType {
  Add =  'add',
  Concat = 'concat'
};

interface AddActionPayload {
  num1: number;
  num2: number;
}

interface ConcatActionPayload {
  str1: string;
  str2: string;
}

interface AddAction {
    type: ActionType.Add;
    payload: AddActionPayload;
}

interface ConcatAction {
    type: ActionType.Concat;
    payload: ConcatActionPayload;
}

type Actions = AddAction | ConcatAction;

const payloads: Record<ActionType, Actions['payload']> = {
  add: {
    // accept only AddActionPayload
  },
  concat: {
    // accept only ConcatActionPayload
  }
}

Any idea how to achieve this?

CodePudding user response:

The Record<K, V> utility type is a mapped type in which the key and value types are completely uncorrelated. It's of the form type Record<K, V> = {[P in K]: V}, where the key type of each key P (which iterates over the union of members of K) does not have any effect on the corresponding value type, which is V no matter what. So you get behavior you don't like:

type Oops = Record<ActionType, Actions['payload']>;
/* type Oops = {
    add: AddActionPayload | ConcatActionPayload;
    concat: AddActionPayload | ConcatActionPayload;
} */

Instead, you should write a mapped type in which the value depends explicitly on the key. Since you have an Actions union representing the information you want, the easiest thing to do is to map directly over Actions using key remapping in your mapped type to extract the key type as well as the value type.

It looks like this:

type Payloads = { [T in Actions as T['type']]: T['payload'] };
/* type Payloads = {
    add: AddActionPayload;
    concat: ConcatActionPayload;
} */

That behaves how you'd like:

const payloads: Payloads = {
  add: { num1: 1, num2: 2 },
  concat: { str1: "a", str2: "b" }
}; // okay

const badPayloads: Payloads = {
  add: { str1: "a", str2: "b" },
  concat: { num1: 1, nume2: 2 }
};  // error!

Playground link to code

  • Related