与 Typescript 相交的类型

gmw*_*934 3 types intersection typescript

我有两种自定义类型

type TypeA = string | number;
type TypeB = string | boolean;
Run Code Online (Sandbox Code Playgroud)

我使用上述创建了一个交叉类型

type Combined = TypeA & TypeB;
Run Code Online (Sandbox Code Playgroud)

自然,该Combined类型只会是 type string,因为它是唯一与TypeA和相交的类型TypeB

但是,如果我改变工会TypeB,并添加一个日期类型,我得到一个意外的行为,例如:

type TypeA = string | number;
type TypeB = string | boolean | Date;
Run Code Online (Sandbox Code Playgroud)

创建新的交叉点类型

type Combined = TypeA & TypeB;
Run Code Online (Sandbox Code Playgroud)

如果我检查类型的签名,它看起来像这样

type Combined = string | (string & Date) | (number & Date)
Run Code Online (Sandbox Code Playgroud)

我的问题是为什么会这样?这是预期的吗?我认为它是string类型,因为它是唯一相交的类型

Tit*_*mir 6

这是预期的行为,基元的交集被简化为never,而基元与对象类型的交集没有被简化(以启用诸如品牌基元类型之类的东西)。Date不是原语,它是 lib.d.ts 中定义的对象类型

考虑到这一点,事实上打字稿通过将交集移动到内部来标准化并集和交集,我们得到

type TypeA = string | number;
type TypeB = string | boolean;

type Combined = TypeA & TypeB;
 => (string | number) & (string | boolean)
 // Distributivity kicks in 
 => (string & string) | (string & boolean) | (number & string) | (number & boolean)
 //  intersection simplification
 => string | never | never | never 
 // never melts away in a union
 => string 

Run Code Online (Sandbox Code Playgroud)

而在第二种情况下我们得到

type TypeA = string | number;
type TypeB = string | boolean | Date;

type Combined = TypeA & TypeB;
 => (string | number) & (string | boolean | Date)
 // Distributivity kicks in 
 => (string & string) | (string & boolean) | (string & Date) | (number & string) | (number & boolean) | (number & Date)
 //  intersection simplification, but nothing is done about Date and any primitive
 => string | never | (string & Date) never | never | (number & Date)
 // never melts away in a union
 => string 

Run Code Online (Sandbox Code Playgroud)

TypeA如果您想从中提取任何类型,TypeB最好使用Extract条件类型

type TypeA = string | number;
type TypeB = string | boolean | Date;

type Combined = Extract<TypeA, TypeB>;
Run Code Online (Sandbox Code Playgroud)

游乐场链接


jca*_*alz 5

这不是错误;它允许一个(未广为人知的)功能称为品牌标记原语。


v在运行时拥有一个值,它既是一个string(原始的, where typeof v === "string",不是一个String包装对象)又是一个Date. 在某种意义上,类型string & Date确实与 相同never,因为您可以将 TypeScript 类型视为所有合适的 JavaScript 值的集合。如果没有string & Date值,也没有never值,那么这些类型在逻辑上是等价的。

因此,如果编译器是热切减少像一个路口这将是合理的string & Datenever。它已经为不兼容的单元类型的交集(如"a" & 0)和不兼容的原语(如microsoft/TypeScript#31838 中string & number实现的)的交集执行此操作。那么为什么当我们将对象类型与基元相交时不会发生这种情况呢?


原因是允许一个称为品牌原语的功能,它模拟TypeScript 中原语的名义类型(在这个常见问题条目中提到)。

TypeScript 大多只有结构类型类型别名;如果两种类型具有相同的结构但名称不同,则它们是相同的类型。如果您通过类型别名为现有类型指定一个新名称,则它们是相同的类型。通常这正是您想要的,但有时您想提出两种类型,虽然在运行时相同,但需要在代码中加以区分,因为您不希望开发人员不小心将它们混淆。

例如(这可能是一个愚蠢的例子):

type Username = string;
type Password = string;
declare function login(username: Username; password: Password): void;
Run Code Online (Sandbox Code Playgroud)

在这里,我们要确保编写调用 TypeScript 代码的login人不会意外地将密码输入用户名,反之亦然。如果上述类型别名实际上阻止您执行此操作,那就太好了:

declare function getUsername(): Username;
declare function getPassword(): Password;
login(getPassword(), getUsername()); // no error, OOPS
Run Code Online (Sandbox Code Playgroud)

但事实并非如此。该UsernamePassword类型都只有string。我们使用不同名称的事实并没有改变这一点。因此,有时 TypeScript 开发人员希望他们的类型是名义类型,以捕获上述错误。

microsoft/TypeScript#202 中有一个关于如何获得名义类型的很长的讨论。对原始类型执行此操作的一种方法是使用“标记”,在其中添加仅存在于类型系统中而不存在于运行时的“幻影”区分属性。因此,您可以将上述内容更改为:

type Username = string & { __brand: "Username" };
type Password = string & { __brand: "Password" };
Run Code Online (Sandbox Code Playgroud)

突然之间你会在这里得到想要的错误:

login(getPassword(), getUsername()); // error! Password not assignable to Username
Run Code Online (Sandbox Code Playgroud)

当然,实际上说服编译器一个特定的string确实是 aUsername或 aPassword包括通过诸如类型断言之类的东西说谎

function toUsername(x: string): Username {
    return x as Username; // <-- lying
}
Run Code Online (Sandbox Code Playgroud)

但是我们当然知道在运行时您不能真正拥有类型Usernameor的值Password,因为如果您采用原语string,它将没有__brand属性。如果编译器决定急切地将这些不可能在运行时标记的类型减少到never,它们就会完全崩溃。这比仅使用原语更糟糕,因为没有任何东西可以分配给它们,但它们仍然无法区分并允许混合:

login(getPassword(), getUsername()); // no error again
Run Code Online (Sandbox Code Playgroud)

虽然此功能可能不是很受欢迎,但它已被用于现有的实际 TypeScript 代码,包括TypeScript 编译器本身TypeScript 源代码。将品牌原语减少到never 会破坏太多人,以至于不值得。


Playground 链接到代码