问题出在这一行:(FreeLog & UndefinedTask)。
上面的交叉点产生这种类型:
type Debug<T> = {
[Prop in keyof T]: T[Prop]
}
// type Result = {
// message: string | Error;
// meta?: unknown;
// task?: undefined;
// }
type Result = Debug<FreeLog & UndefinedTask>
我们最终得到了一个必需的属性:message。让我们测试一下:
function foo(ev: RuntimeEvent) {
console.log(ev);
}
foo({ message: '2' });
但为什么它也允许type? { message: string, type: 'log' } 不是任何联合的类型。
type Check<T> = T extends RuntimeEvent ? true : false
// true
type Result = Check<{ message: 'string', type: 'log' }>
{ message: 'string', type: 'log' } 扩展了RuntimeEvent,因为FreeLog & UndefinedTask 是其中的一部分,它希望至少一个属性message 满足最低要求。
我们知道为什么它是允许的,但是type 呢?为什么只允许使用log 值?因为当您开始输入type 属性时,TS 开始检查您的参数。似乎只有与log 的联合具有message 和type 属性。
您可能提供也可能不提供task 属性。它是由你决定。 TS 不会抱怨,因为从技术上讲,您的论点符合要求。
要使其以您期望的方式工作,您可以使用StrictUnion helper:
// credits goes to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
完整示例:
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
interface TaskStartedEvent {
type: "started",
task: string
}
interface TaskLogEvent {
type: "log",
task: string,
message: string
}
interface TaskFailedEvent {
type: "failed",
task: string,
error?: string
}
interface FreeLog {
message: string | Error,
meta?: unknown
}
interface UndefinedTask {
task?: undefined
}
type TaskEvent = TaskStartedEvent | TaskLogEvent | TaskFailedEvent;
type RuntimeEvent = StrictUnion<(FreeLog & UndefinedTask) | TaskEvent>;
function foo(ev: RuntimeEvent) {
console.log(ev);
}
foo({ message: 'string', type: 'log' }); // expected error
Playground
你可以在我的blog找到更多有趣的例子
总结
FreeLog & UndefinedTask 不希望您提供task 属性。至少 task 不是必需的,而 TaskEvent 需要 task。所以,你最终遇到了一个联合中有两个元素的情况。一个元素需要task,而另一个不需要。
.. 与受歧视联合的行为方式不一致...
请记住,您的工会不受歧视。在UndefinedTask 中制作task 所需的道具。会有帮助的。
歧视性工会
如果你想使用discriminated union,也使用tagged unions,你应该为联合的每个元素创建一个type。对于联合中的每个元素,type 属性应该不同。
见例子:
interface TaskStartedEvent {
type: "started",
task: string
}
interface TaskLogEvent {
type: "log",
task: string,
message: string
}
interface TaskFailedEvent {
type: "failed",
task: string,
error?: string
}
interface FreeLog {
message: string | Error,
meta?: unknown
}
interface UndefinedTask {
task: undefined
}
type UndefinedEvent = (FreeLog & UndefinedTask) & {
type: 'undefined'
}
type TaskEvent = TaskStartedEvent | TaskLogEvent | TaskFailedEvent;
type RuntimeEvent = UndefinedEvent | TaskEvent;
function foo(ev: RuntimeEvent) {
console.log(ev);
}
foo({ message: "bar", type: "log" }); // error
另外,请查看文档中的示例:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };
属性kind 用作联合中每个元素的标记。
例如,F# 也使用discriminated unions:
type Shape =
| Rectangle of width : float * length : float
| Circle of radius : float
| Prism of width : float * float * height : float
您可能已经注意到,Rectangle、Circle 和 Prism 只是标签。