TypeScript [功能请求]联合类型运算符优先选择窄类型而非宽类型

kokeuurv  于 4个月前  发布在  TypeScript
关注(0)|答案(5)|浏览(60)

建议

🔍 搜索词

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 表达式”。是的。但它们太严格了。
为了解决这个问题,我建议 |& 操作符,它更像常规的 | ,但在可能的情况下会优先选择更窄的类型(当对象的形状满足缩小后的变体时,即它可以分配给更窄的变体)。它将需要为表达式的两边实现处理(在这个例子中,对于 RouteRouteWithNavTitle )。
P.S.非常感谢您阅读这篇文章。请留下您的评论让我了解是否遗漏了什么或解释错误。

hgncfbus

hgncfbus1#

关于angular风格的示例问题,我感到困惑。据我所知,唯一导致错误未被注意到的原因是 RouterRouterWithNavTitle 都具有名为 data 的属性,以及Router的 data: any 。如果你更改了名称或为 Router.data 提供了一个真正的类型,问题就会消失。

我认为 |& 是用来解决这个问题的,但你需要解释它是如何处理 any 的,严格来说,这并不是一个“窄”或“宽”类型的限制--更像是一种禁用类型检查的类型。

yacmzcpb

yacmzcpb2#

如果你更改了Router.data的名称或给它一个真正的类型,问题就会消失。我无法做到这两点。
Any 实际上是最宽的类型,因为我们可以给它分配任何值。

处理 Any

让我们考虑两种情况:分配和使用。

分配:

TypeScript应该按照以下方式处理分配:

  1. 假设值属于更宽的类型
  2. 找出哪个类型更窄(在我们的例子中是 RouteWithNavTitle)
  3. 计算窄类型和宽类型之间的差异,并提供不同字段的类型列表( path , data )
  4. 从值中选择这些字段,并尝试将其分配给更窄的类型(只剩下这些字段:
    Pick<RouteWithNavTitle, 'path'|'data'> )
  5. 如果至少有一个字段可以分配(即hits),并且至少有一个必需的字段不存在,我们认为值为更窄的类型(如果存在则产生错误)

注意:缺失字段的类型是 unknown

示例1(提供了 data 字段和 hits,缺少 path;对于可选的 data 也是如此):

type RouteWithNavTitle = RequiredProps<Route, "path"> & {data: {navTitle: string}}
type RouteWithOptionalNavTitle = Route |& RouteWithNavTitle

const test: RouteWithOptionalNavTitle[] = [
// at first, `test` considered as Route
// Then difference between Route and RouteWithNavTitle is computed. 
// Fields, type of which differs, are: `path`, `data`.
// Selecting `path` ('cause missing, type is `unknown`) and `data` field of `test` variable and trying to assign to 
// Pick<RouteWithNavTitle, "path"|"data">
// `data` property hits. `path` is **not** optional. Now consider `test` as `RouteWithNavTitle `
// Produce error about no `path` field present
  {
    data: {
        navTitle: 'asd'
    }
  }
]

示例2(提供了 data 字段但缺少):

type RouteWithNavTitle = RequiredProps<Route, "path"> & {data: {navTitle: string}}
type RouteWithOptionalNavTitle = Route |& RouteWithNavTitle

const test: RouteWithOptionalNavTitle[] = [
// at first, `test` considered as Route
// Then difference between Route and RouteWithNavTitle is computed. 
// Fields, type of which differs, are: `path`, `data`.
// Selecting `path` and `data` field of `test` variable and trying to assign to 
// Pick<RouteWithNavTitle, "path"|"data">
// `data` property misses. 
// No hits at all, so it is defently not a RouteWithNavTitle 
  {
    data: {
        foobar: 'asd'
    }
  }
]

注意:如果缺少必需的字段,则生成错误;如果缺少可选字段,则在可分配的情况下视为更宽的类型

示例3(提供了必需的 path 字段和可选的 data 字段,缺少):

type RouteWithNavTitle = RequiredProps<Route, "path"> & {data?: {navTitle: string}}
type RouteWithOptionalNavTitle = Route |& RouteWithNavTitle

const test: RouteWithOptionalNavTitle[] = [
// at first, `test` considered as Route
// Then difference between Route and RouteWithNavTitle is computed. 
// Fields, type of which differs, are: `path`, `data`.
// Selecting `path` and `data` (`data` is `unknown` bacause not present) 
// field of `test` variable and trying to assign to 
// Pick<RouteWithNavTitle, "path"|"data">
// `path` property hits. But `data` misses
// So consider `test` as Route, because `data` is optional
  {
    path: "foobar"
  }
]

使用:

在函数内部,假设 routerConfigRoute ,除非访问不同的字段。要访问这些字段,开发人员必须为它们提供类型保护(TypeScript应该强制他们这样做)

type RouteWithNavTitle = RequiredProps<Route, "path"> & {data: {navTitle: string}}
type RouteWithOptionalNavTitle = Route |& RouteWithNavTitle

functions foobar(routerConfig: RouteWithOptionalNavTitle ) {
  // Error: path is optional in `Route`
  routerConfig.path;
  // Error: Provided only in RouteWithNavTitle. Please, use type guard to check if property is present
  routerConfig.data.navTitle;

 // unambiguously resolves to RouteWithNavTitle
 if (routerConfig.data.navTitle) {
    // its ok. Inside this branch assumes that `routerConfig` is `RouteWithNavTitle`
    routerConfig.data.navTitle;
    routerConfig.path;
  }

  // ambiguously roslves to Route and RouteWithNavTitle so prefer Route (wider type)
  if (routerConfig.path) {
    routerConfig.path; // ok
    routerConfig.data.navTitle // Error: not Present in Route type
  }

}
qaxu7uf2

qaxu7uf23#

为什么是"|&"而不是其他符号?这是因为在提供者端(即变量声明)上,这种行为类似于"&",或者更接近于从最窄到最宽的函数重载排序(尝试分配给更窄的类型,如果失败,则转到下一个更宽的类型,就像对于函数重载一样)。而在消费者端,更像"|",强制实现运行时类型检查,以支持复合类型的某一方面。

w1jd8yoj

w1jd8yoj4#

看起来我在zig-lang中找到了这个特性的类似物。它被称为带标签的联合体。它的工作原理类似于枚举,但附加字段集取决于当前枚举值。
参考:https://ziglang.org/documentation/master/#Tagged-union

nfg76nw0

nfg76nw05#

我上面提到的使用案例强调了,带有标签的联合类型对于提供类型安全的配置和表达一个字段依赖于另一个字段是非常有用的。

相关问题