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!