英文:
Rely on a mapping to value each variant of a discriminated union
问题
以下是您要翻译的部分:
"When consuming all variants of a discriminated union with some if
statements, TypeScript narrows down the type to the specifics of the variant. I understand that to do the same while expressing the logic with a mapping from the discriminant to a function to process this variant, the mapping should be seen as distributive type, and not just a mapped type. I read https://stackoverflow.com/a/73262902/2924547 and https://github.com/microsoft/TypeScript/pull/47109, but still, I do not manage to apply this to my use case.
type Message =
| { kind: "mood"; isHappy: boolean }
| { kind: "age"; value: number };
// Could be derived from `Message` with
// type ExtractMessage<M, P> = M extends { kind: P } ? Omit<M, "kind"> : never;
//
// type Mapping = {
// [key in Message["kind"]]: (
// payload: Omit<ExtractMessage<Message, key>, "kind">
// ) => void;
// };
type Mapping = {
mood: (payload: { isHappy: boolean }) => void;
age: (payload: { value: number }) => void;
};
// OK: TypeScript complains if a key is missing or the signature is wrong
const mapping: Mapping = {
mood: ({ isHappy }) => {
console.log(isHappy);
},
age: ({ value }) => {
console.log(value + 1);
},
};
const process = (message: Message) => {
// NOT OK: TypeScript complains because it does not now we are dealing with the right message
mapping[message.kind](message);
};
英文:
When consuming all variants of a discriminated union with some if
statements, TypeScript narrows down the type to the specifics of the variant. I understand that to do the same while expressing the logic with a mapping from the discriminant to a function to process this variant, the mapping should be seen as distributive type, and not just a mapped type. I read https://stackoverflow.com/a/73262902/2924547 and https://github.com/microsoft/TypeScript/pull/47109, but still, I do not manage to apply this to my use case.
type Message =
| { kind: "mood"; isHappy: boolean }
| { kind: "age"; value: number };
// Could be derived from `Message` with
// type ExtractMessage<M, P> = M extends { kind: P } ? Omit<M, "kind"> : never;
//
// type Mapping = {
// [key in Message["kind"]]: (
// payload: Omit<ExtractMessage<Message, key>, "kind">
// ) => void;
// };
type Mapping = {
mood: (payload: { isHappy: boolean }) => void;
age: (payload: { value: number }) => void;
};
// OK: TypeScript complains if a key is missing or the signature is wrong
const mapping: Mapping = {
mood: ({ isHappy }) => {
console.log(isHappy);
},
age: ({ value }) => {
console.log(value + 1);
},
};
const process = (message: Message) => {
// NOT OK: TypeScript complains because it does not now we are dealing with the right message
mapping[message.kind](message);
};
答案1
得分: 1
以下是翻译的内容:
在microsoft/TypeScript#47109中描述的重构希望您将所有内容都写成一个相当简单的“基本”对象类型,例如
interface Mapping {
mood: {
isHappy: boolean;
};
age: {
value: number;
};
}
在您的情况下,可以从您的原始Message
类型生成,我会将其重命名:
type _Message =
| { kind: "mood"; isHappy: boolean }
| { kind: "age"; value: number };
type Mapping = { [T in _Message as T["kind"]]:
{ [K in keyof T as Exclude<K, "kind">]: T[K] }
}
其他类型应该要么是在该对象类型上的映射类型,例如
type MappingCallbacks =
{ [K in keyof Mapping]: (payload: Mapping[K]) => void }
const mappingCallbacks: MappingCallbacks = {
mood: ({ isHappy }) => {
console.log(isHappy);
},
age: ({ value }) => {
console.log(value + 1);
},
};
要么是作为分布式对象类型,这是可以立即进行索引访问以获取联合类型的映射类型,例如:
type Message<K extends keyof Mapping = keyof Mapping> =
{ [P in K]: { kind: P } & Mapping[P] }[K]
type M = Message
/* type M =
({ kind: "mood"; } & { isHappy: boolean; }) |
({ kind: "age"; } & { value: number; })
*/
然后,您的操作必须是泛型的,基本接口的键类型:
const process = <K extends keyof Mapping>(message: Message<K>) => {
mappingCallbacks[message.kind](message);
};
尽管从概念上讲,非泛型版本的process
应该可以工作,其中message
只是一个联合类型,但编译器无法遵循不同联合类型的每个成员都符合相似的约束这一逻辑。实际上,正是由于无法使用纯联合类型实现这一点,才出现了microsoft/TypeScript#30581中描述的问题,而microsoft/TypeScript#47109正是为了解决这个问题而编写的。
英文:
The refactoring described in microsoft/TypeScript#47109 wants you to write everything in terms of a fairly simple "basic" object type like
interface Mapping {
mood: {
isHappy: boolean;
};
age: {
value: number;
};
}
which, in your case, could be generated from your original Message
type, which I'll rename out of the way:
type _Message =
| { kind: "mood"; isHappy: boolean }
| { kind: "age"; value: number };
type Mapping = { [T in _Message as T["kind"]]:
{ [K in keyof T as Exclude<K, "kind">]: T[K] }
}
Other types should either be mapped types on that object type, such as
type MappingCallbacks =
{ [K in keyof Mapping]: (payload: Mapping[K]) => void }
const mappingCallbacks: MappingCallbacks = {
mood: ({ isHappy }) => {
console.log(isHappy);
},
age: ({ value }) => {
console.log(value + 1);
},
};
or as distributive object types which are mapped types into which one has immediately indexed with all the keys to get a union, such as:
type Message<K extends keyof Mapping = keyof Mapping> =
{ [P in K]: { kind: P } & Mapping[P] }[K]
type M = Message
/* type M =
({ kind: "mood"; } & { isHappy: boolean; }) |
({ kind: "age"; } & { value: number; })
*/
Then your operations must be generic in the key type of your basic interface:
const process = <K extends keyof Mapping>(message: Message<K>) => {
mappingCallbacks[message.kind](message);
};
Even though conceptually the non-generic version of process
should work, where message
is just a union, the compiler just can't follow that logic that the different members of the union each obey an analogous constraint. Indeed, it was the failure to do this with pure unions that was the subject of microsoft/TypeScript#30581, the problem which microsoft/TypeScript#47109 was written to solve.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论