【问题标题】:Expecting an error but code compiles fine期待一个错误,但代码编译得很好
【发布时间】:2021-11-15 13:15:57
【问题描述】:

鉴于此代码,

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 = (FreeLog & UndefinedTask) | TaskEvent;

function foo(ev: RuntimeEvent) {
    console.log(ev);    
}
foo({ message: "bar", type: "log" });

为什么 Typescript 编译器在这里没有失败?

我传递了 type 字段,所以它不能是 (FreeLog & UndefinedTask) 类型,但我没有传递 task 字段,所以它也不能是 TaskEvent

此代码编译没有错误(typescriptlang.org 链接)。

【问题讨论】:

  • 这是minimal example。看来Object literal may only specify known properties 错误只会在指定联合中的 any 类型中不存在的字段时引发。不确定这是否是预期行为。
  • 还值得注意的是,虽然这个 看起来 像一个可区分的联合,但并不是因为FreeLog 没有指定常量字符串类型。
  • @superhawk610 是的,我知道这不是一个适当的歧视性工会。我仍然希望它能够正确运行。对我来说这看起来像是一个错误,但我不确定。
  • 您预计会出现什么错误,在哪里?
  • @captain-yossarian 当我将{ message: "bar", type: "log" } 传递给foo 时,期待unexpected field typeexpected field task 之类的东西

标签: typescript


【解决方案1】:

问题出在这一行:(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 &amp; UndefinedTask 是其中的一部分,它希望至少一个属性message 满足最低要求。

我们知道为什么它是允许的,但是type 呢?为什么只允许使用log 值?因为当您开始输入type 属性时,TS 开始检查您的参数。似乎只有与log 的联合具有messagetype 属性。

您可能提供也可能不提供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 &amp; 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

您可能已经注意到,RectangleCirclePrism 只是标签。

【讨论】:

  • 严格的联合技巧有效,但我想我仍然想知道You may or may not provide task property. It is up to you. TS does not complain because technically, your argument meet requirements.。为什么定期工会会出现这种情况?毕竟,如果我提供type: "log",编译器应该期望我提供TaskLogEvent 中的所有字段,不是吗?
  • 看起来,因为FreeLog &amp; UndefinedTaskTaskLogEvent都希望我提供task,只满足其中一个就足够了(例如,只满足@设置的task要求987654371@),但这对我来说仍然有点不直观,因为它似乎与有区别的联合的行为方式不一致(如果我将 task 内的 UndefinedTask 设为实际字符串,它将按预期工作)
  • 所以做一个严格联合(不使用那个助手)的唯一方法是有一个完全区分的联合(不像我做的那样)。它看起来仍然像我希望 TS 编译器处理的东西,但我想这是有道理的。
  • 最好坚持使用有区别的工会。
猜你喜欢
  • 1970-01-01
  • 2015-10-28
  • 1970-01-01
  • 1970-01-01
  • 2011-08-29
  • 2016-10-24
  • 2017-11-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多