预计会出现错误,但代码可以正常编译

are*_*ler 5 typescript

鉴于此代码,

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" });
Run Code Online (Sandbox Code Playgroud)

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

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

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

cap*_*ian 3

问题出在这一行:(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>
Run Code Online (Sandbox Code Playgroud)

我们最终得到了一个必需的属性:message。我们来测试一下:

function foo(ev: RuntimeEvent) {
    console.log(ev);
}

foo({ message: '2' });
Run Code Online (Sandbox Code Playgroud)

但为什么还允许呢type{ message: string, type: 'log' }不是任何联合的类型。

type Check<T> = T extends RuntimeEvent ? true : false

// true
type Result = Check<{ message: 'string', type: 'log' }>
Run Code Online (Sandbox Code Playgroud)

{ message: 'string', type: 'log' }扩展是RuntimeEvent因为FreeLog & UndefinedTask它是它的一部分,并且它期望至少有一个属性message满足最低要求。

我们知道为什么允许这样做,但是呢type?为什么只允许使用log值?因为当你开始输入type属性时,TS 就开始检查你的参数。看来只有 union withlog具有messagetype属性。

您可以提供也可以不提供task财产。它是由你决定。TS 没有抱怨,因为从技术上来说,你的论点符合要求。

为了使其按照您期望的方式工作,您可以使用StrictUnion助手:

// 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>
Run Code Online (Sandbox Code Playgroud)

完整示例:

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

Run Code Online (Sandbox Code Playgroud)

操场

你可以在我的博客中找到更多有趣的例子

概括

FreeLog & UndefinedTask不期望您提供该task财产。至少task不是必需的,而TaskEvent需要task。所以,你最终遇到了联合中有两个元素的情况。一个元素需要task,而另一个则不需要。

..与受歧视工会的行为方式不一致...

请记住,您的工会不受歧视。在 中制作task所需的道具UndefinedTask。我会帮你的。

区分联合 如果您想使用区分联合,也标记联合,您应该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
Run Code Online (Sandbox Code Playgroud)

另外,请查看文档中的示例:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };
Run Code Online (Sandbox Code Playgroud)

属性kind充当联合中每个元素的标签。例如,F# 还使用可区分联合

type Shape =
    | Rectangle of width : float * length : float
    | Circle of radius : float
    | Prism of width : float * float * height : float
Run Code Online (Sandbox Code Playgroud)

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