nullish coalescing operator removing one part of the union of records

huangapple go评论61阅读模式
英文:

why does the nullish coalescing operator removing one part of the union of records

问题

I've been scratching my head without finding an answer. I don't understand why the nullish coalescing operator removes one part of the union in the following snippet:

declare const thing:
  | { id: number; version: number; label: string }
  | {
      label: string;
    }
  | undefined;

// it yields {label: string} | null instead of the expected { id: number; version: number; label: string } | { label: string; } | null
const thingWithCoallesce = thing ?? null;

I created a playground to demonstrate the case. I couldn't find anything in the issue tracker about that.

英文:

I've been scratching my head without finding an answer. I don't understand why the nullish coalescing operator removes one part of the union in the following snippet:

declare const thing:
  | { id: number; version: number; label: string }
  | {
      label: string;
    }
  | undefined;

// it yields {label: string} | null instead of the expected { id: number; version: number; label: string } | { label: string; } | null
const thingWithCoallesce = thing ?? null;

I created a playground to demonstrate the case. I couldn't find anything in the issue tracker about that.

答案1

得分: 1

以下是您要翻译的内容:

这与nullish coalescing无关。编译器正在执行子类型缩减,无论何时都可以执行;请参阅microsoft/TypeScript#49952获取更多信息。


让我们给这些类型取个名字。 您的代码与以下代码基本相同:

interface Base {
  label: string
}

interface Subtype extends Base {
  id: number;
  version: number;
}

declare const thing: Subtype | Base | undefined
// const thing: Base | Subtype | undefined

const thingWithCoallesce = thing ?? null;
// const thingWithCoallesce: Base | null

就我们的目的而言,类型Subtype等效于{id: number; version: number; label: string},而类型Base等效于{label: string}


类型SubtypeBase子类型(因此命名为如此);每个类型Subtype的值也是类型Base的值:

const s: Subtype = { id: 1, version: 2, label: "abc" };
const b: Base = s; // okay

(请注意,excess property checking有时会让人们以为情况不同;请参阅https://stackoverflow.com/q/72819923/2887218获取更多信息)。

这意味着联合类型Subtype | Base等效于Base。您可以从关于联合的一些规则中看到这一点,例如:X extends (X | Y)以及:如果X extends ZY extends Z,则(X | Y) extends Z。因此,(Subtype | Base) extends Base以及Base extends (Subtype | Base),因此它们是等效的。当然,TypeScript的类型系统并不完全sound,因此有时候“等效”的事物在某些情况下可能会表现出不同的行为。


好的,Subtype | BaseBase是等效的类型。因此,编译器可以在它“想要”的任何时候执行子类型缩减,并将Subtype | Base实际折叠为Base。实际上,编译器是否以及何时“想要”执行子类型缩减实际上是一个实现细节:

declare const thing: Subtype | Base | undefined
// const thing: Base | Subtype | undefined

但切换/缩小操作(如nullish coalescing)往往会减少联合:

const thingWithCoallesce = thing ?? null;
// const thingWithCoallesce: Base | null

如果这是不希望的行为,那么您可能应该更改类型,以便“每个Subtype都是有效的Base”不适用。如果通过{label: string},您指的是“具有label不具有idversion”,那么您需要明确说明:

interface OtherSubtype extends Base {
  id?: never;
  version?: never;
}

declare const thing: Subtype | OtherSubtype | undefined
// const thing: Subtype | OtherSubtype | undefined

const thingWithCoallesce = thing ?? null;
// const thingWithCoallesce: Subtype | OtherSubtype | null

子类型缩减不会发生,因为既不是Subtype也不是OtherSubtype是对方的子类型。

英文:

This isn't specific to nullish coalescing. The compiler is performing subtype reduction, which it can do whenever it "wants" to; see microsoft/TypeScript#49952 for more information.


Let's give these types names. Your code is essentially identical to:

interface Base {
  label: string
}

interface Subtype extends Base {
  id: number;
  version: number;
}

declare const thing: Subtype | Base | undefined
// const thing: Base | Subtype | undefined

const thingWithCoallesce = thing ?? null;
// const thingWithCoallesce: Base | null

For our purposes, the type Subtype is equivalent to {id: number; version: number; label: string}, while the type Base is equivalent to {label: string}.


The type Subtype is a subtype of Base (hence the naming); every value of type Subtype is also a value of type Base:

const s: Subtype = { id: 1, version: 2, label: "abc" };
const b: Base = s; // okay

(Note that excess property checking sometimes confuses people into thinking otherwise; see https://stackoverflow.com/q/72819923/2887218 for more information).

And that means that the union type Subtype | Base is equivalent to Base. You can see that from following some rules about unions like: X extends (X | Y) and: if X extends Z and Y extends Z then (X | Y) extends Z. So (Subtype | Base) extends Base and Base extends (Subtype | Base) and thus they are equivalent. Of course TypeScript's type system isn't fully sound so sometimes "equivalent" things can behave differently in some situations.


Okay, so Subtype | Base and Base are equivalent types. And so the compiler may, whenever it wants, perform subtype reduction and actually collapse Subtype | Base to Base. It is effectively an implementation detail of the compiler whether and when it "wants" to perform subtype reduction. It happens that when you explicitly create the union it tends to stick around where you've defined it:

declare const thing: Subtype | Base | undefined
// const thing: Base | Subtype | undefined

But switching/narrowing operations like nullish coalescing tend to reduce the unions:

const thingWithCoallesce = thing ?? null;
// const thingWithCoallesce: Base | null

If this is undesirable behavior, then you should probably change your types so that "every Subtype is a valid Base" doesn't apply. If by {label: string} you meant "has a label but lacks an id and a version", then you will need to say so explicitly:

interface OtherSubtype extends Base {
  id?: never;
  version?: never;
}

declare const thing: Subtype | OtherSubtype | undefined
// const thing: Subtype | OtherSubtype | undefined

const thingWithCoallesce = thing ?? null;
// const thingWithCoallesce: Subtype | OtherSubtype | null

And the subtype reduction doesn't happen because neither Subtype nor OtherSubtype are subtypes of the other.

Playground link to code

huangapple
  • 本文由 发表于 2023年5月22日 03:09:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/76301504.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定