建议
🔍 搜索词
narrow, types, type, wide. narrowing
✅ 可实现性检查清单
我的建议符合以下准则:
- 这不会对现有的TypeScript/JavaScript代码造成破坏性的改变
- 这不会改变现有JavaScript代码的运行时行为
- 这可以在不根据表达式的类型发射不同的JS的情况下实现
- 这不是一个运行时特性(例如库功能、带有JavaScript输出的非ECMAScript语法、新的JS语法糖等)
- 这个特性将与TypeScript's Design Goals的其他部分保持一致。
⭐ 建议
当使用|
类型操作符来交替类型时,有时将其行为更改为更窄的类型比更宽的类型更有用(为了更好地理解,请查看下面的示例)。这种类型的行为将需要为表达式的两边实现处理。
📃 动机示例
这个特性将有助于更好地反映一个字段对另一个字段的依赖关系(当两个字段都是可选的,但如果其中一个指定了,另一个也应该指定)。
💻 用例
让我们想象一下(顺便说一下,这是一个现实世界的例子),我们想要根据Web应用程序路由器的配置自动配置导航栏。因此,当navTitle
属性存在并包含一个字符串时,我们对该元素感兴趣,即它将被放置在导航栏上,其中包含在navTitle
属性中的标签。它应该指向某个地方,对吧?但是要到哪里去?要弄清楚这一点,我们应该知道路径。因此,navTitile
属性依赖于path
(这在angular中默认是可选的)。好的!这不是一个困难的问题:
type Router = {path?: string, data?: any}
type RouterWithNavTitile = (RequiredProps<Router, "path"> & {navTitle: string})
type CustomRouterConfig = Router | RouterWithNavTitile
export type RequiredProps<T, P extends keyof T> = Omit<T, P> & Pick<Required<T>, P>
const test1: RouterWithNavTitile = {navTitle: "asd"}
const test: CustomRouterConfig[] = [
// all works as expected
{path: "asd"},
{navTitle:"asd", path:"asd"},
// here is error. yeap. when navTitle present, path should be too but it is not here
{navTitle:"asd"},
{foo: "asd"}
]
解决方案看起来很不错。除非你正在考虑,我们应该将所有其他数据放入data
属性中(这是angular的方式)。让我们尝试一下:
type RouteWithNavTitle = RequiredProps<Route, "path"> & {data: {navTitle: string}}
type RouteWithOptionalNavTitle = Route | RouteWithNavTitle
const test: RouteWithOptionalNavTitle[] = [
// woops! No error! Path is required and not present! And no error
{
data: {
navTitle: 'asd'
}
}
]
为什么?这就是原因! Route | RouteWithNavTitle
将始终解析为 Route
,因为 RouteWithNavTitle
是缩小了 Route
(记得 data
属性吗?在路由中它是 any
,但在 RouteWithNavTitle
中它被缩小到了 {navTitile: string}
)。Typescript将始终优先选择更宽泛的类型以满足兼容性原因。这并不总是方便,就像考虑到的示例那样。
你可能会说:“有一个 const
表达式”。是的。但它们太严格了。
为了解决这个问题,我建议 |&
操作符,它更像常规的 |
,但在可能的情况下会优先选择更窄的类型(当对象的形状满足缩小后的变体时,即它可以分配给更窄的变体)。它将需要为表达式的两边实现处理(在这个例子中,对于 Route
和 RouteWithNavTitle
)。
P.S.非常感谢您阅读这篇文章。请留下您的评论让我了解是否遗漏了什么或解释错误。
5条答案
按热度按时间hgncfbus1#
关于angular风格的示例问题,我感到困惑。据我所知,唯一导致错误未被注意到的原因是
Router
和RouterWithNavTitle
都具有名为data
的属性,以及Router的data: any
。如果你更改了名称或为Router.data
提供了一个真正的类型,问题就会消失。我认为
|&
是用来解决这个问题的,但你需要解释它是如何处理any
的,严格来说,这并不是一个“窄”或“宽”类型的限制--更像是一种禁用类型检查的类型。yacmzcpb2#
如果你更改了Router.data的名称或给它一个真正的类型,问题就会消失。我无法做到这两点。
Any
实际上是最宽的类型,因为我们可以给它分配任何值。处理
Any
让我们考虑两种情况:分配和使用。
分配:
TypeScript应该按照以下方式处理分配:
RouteWithNavTitle
)path
,data
)Pick<RouteWithNavTitle, 'path'|'data'>
)注意:缺失字段的类型是
unknown
示例1(提供了
data
字段和 hits,缺少path
;对于可选的data
也是如此):示例2(提供了
data
字段但缺少):注意:如果缺少必需的字段,则生成错误;如果缺少可选字段,则在可分配的情况下视为更宽的类型
示例3(提供了必需的
path
字段和可选的data
字段,缺少):使用:
在函数内部,假设
routerConfig
是Route
,除非访问不同的字段。要访问这些字段,开发人员必须为它们提供类型保护(TypeScript应该强制他们这样做)qaxu7uf23#
为什么是"|&"而不是其他符号?这是因为在提供者端(即变量声明)上,这种行为类似于"&",或者更接近于从最窄到最宽的函数重载排序(尝试分配给更窄的类型,如果失败,则转到下一个更宽的类型,就像对于函数重载一样)。而在消费者端,更像"|",强制实现运行时类型检查,以支持复合类型的某一方面。
w1jd8yoj4#
看起来我在
zig-lang
中找到了这个特性的类似物。它被称为带标签的联合体。它的工作原理类似于枚举,但附加字段集取决于当前枚举值。参考:https://ziglang.org/documentation/master/#Tagged-union
nfg76nw05#
我上面提到的使用案例强调了,带有标签的联合类型对于提供类型安全的配置和表达一个字段依赖于另一个字段是非常有用的。