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类型,因为它是唯一相交的类型。
这是预期的行为,基元的交集被简化为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)
这不是错误;它允许一个(未广为人知的)功能称为品牌或标记原语。
v在运行时拥有一个值,它既是一个string(原始的, where typeof v === "string",不是一个String包装对象)又是一个Date. 在某种意义上,类型string & Date确实与 相同never,因为您可以将 TypeScript 类型视为所有合适的 JavaScript 值的集合。如果没有string & Date值,也没有never值,那么这些类型在逻辑上是等价的。
因此,如果编译器是热切减少像一个路口这将是合理的string & Date来never。它已经为不兼容的单元类型的交集(如"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)
但事实并非如此。该Username和Password类型都只有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 会破坏太多人,以至于不值得。
| 归档时间: |
|
| 查看次数: |
1330 次 |
| 最近记录: |