【问题标题】:Define an generic intersection type of 1...n string template types定义 1...n 个字符串模板类型的通用交集类型
【发布时间】:2021-06-18 23:55:58
【问题描述】:

我正在尝试利用字符串模板文字类型的力量来为用于定义路由的字符串添加类型安全性。

  • 例如,没有参数的路由可以是任何string
  • 具有单个参数的路由必须在路径字符串中包含 :parameterName 并在 params 对象中包含 parameterName 作为键/值。

我可以手动设置这些类型,而且效果很好。但我想做的是找到一种方法来消除开发人员手动链接交叉点的需要。我想在我的图书馆里处理它。

type DynamicParam<S extends string> = `:${S}`
type DynamicParamRoute<T extends string> = `${string}/${DynamicParam<T>}/${string}` | `${DynamicParam<T>}/${string}` | `${string}/${DynamicParam<T>}` | `${DynamicParam<T>}`

type UserParamRoute = DynamicParamRoute<'user'>

// const bad1: UserParamRoute = 'user' // error as doesn't match ":user"
const u1: UserParamRoute = ':user'
const u2: UserParamRoute = 'prefix/:user'
const u3: UserParamRoute = ':user/suffix'
const u4: UserParamRoute = 'prefix/:user/suffix'

type TeamParamRoute = DynamicParamRoute<'team'>

const t1: TeamParamRoute = ':team'
const t2: TeamParamRoute = 'prefix/:team'
const t3: TeamParamRoute = ':team/suffix'
const t4: TeamParamRoute = 'prefix/:team/suffix'


// Combining them to get safety on a multi-param route string

type UserTeamParamRoute = UserParamRoute & TeamParamRoute // ***** I don't want my library consumer to be responsible for this. ******

// const ut1: UserTeamParamRoute = 'user/team'  // Type '"user/team"' is not assignable to type 'UserTeamParamRoute'.ts(2322)
// const ut1: UserTeamParamRoute = ':user'      // Type '":user"' is not assignable to type 'UserTeamParamRoute'.ts(2322)
// const ut1: UserTeamParamRoute = ':team'      // Type '":team"' is not assignable to type 'UserTeamParamRoute'.ts(2322)

const ut1: UserTeamParamRoute = ':user/:team'
const ut2: UserTeamParamRoute = 'prefix/:user/:team'
const ut3: UserTeamParamRoute = ':user/:team/suffix'
const ut4: UserTeamParamRoute = ':user/middle/params/:team'
const tu1: UserTeamParamRoute = ':team/:user'
const tu2: UserTeamParamRoute = 'prefix/:team/:user'
const tu3: UserTeamParamRoute = ':team/:user/suffix'
const tu4: UserTeamParamRoute = ':team/middle/params/:user'

我尝试了一种将 keyof 用于 params 对象的方法,但它创建了键的联合。路由定义需要参数对象,特别是如果有动态参数。因此,使用它的键来生成路由的path 所需的类型似乎太完美了。

const params = {
  user: 'someting',
  team: 'someotherthing'
} as const

const ps = ['user', 'team'] as const

type Params = keyof typeof params


// Doesn't work, no intersection to force requiring all the keys. Just union of everything
type RouteParams = DynamicParamRoute<Params> // type RouteParams = ":user" | `${string}/:user/${string}` | `:user/${string}` | `${string}/:user` | ":team" | `${string}/:team/${string}` | `:team/${string}` | `${string}/:team`

const r1: RouteParams = ':user' // means this is valid
const p1: RouteParams = ':team' // so is this
// const rp: RouteParams = 'userteam'
// const rp0: RouteParams = ''
// const rp01: RouteParams = 'missingall'
const rp1: RouteParams = ':user/:team'
const rp2: RouteParams = 'prefix/:user/:team'
const rp3: RouteParams = ':user/:team/suffix'
const rp4: RouteParams = ':user/middle/params/:team'
const pr1: RouteParams = ':team/:user'
const pr2: RouteParams = 'prefix/:team/:user'
const pr3: RouteParams = ':team/:user/suffix'
const pr4: RouteParams = ':team/middle/params/:user'

提前感谢您提供任何和所有见解!

更新:jcalz 解决方案演示为 GIF(我希望它不会太压缩)

[

【问题讨论】:

  • “但我想做的是找到一种方法来消除开发人员手动链接交叉点的需要。我想在我的库中处理它。”这是什么意思?
  • 您是说您事先不知道 URL 模式会是什么样子?可能是foo/barfoo/:bar/baz 或任意序列?
  • 也许您可以提供一个代码示例,说明您如何设想某人理想地使用/使用您的库,因为我不太了解。
  • 嘿,jered,我正在为 React Router 编写一个库,其中包含有关如何插入参数的特定字符串规则。基本上,我想要一种可以从 params 对象中推断出键的类型,并使用该类型来为路径字符串中的每个键要求一个 :${key} 的实例,包括一些其他要求,例如使用 / 进行正确分隔。 jcalz 把它钉在了下面!

标签: typescript


【解决方案1】:

在 TypeScript 中,可以通过某种类型的杂耍来transform unions to intersections,将有问题的联合置于contravariant 位置。下面是我重新定义DynamicParamRoute&lt;T&gt; 的方法,以便T 中的联合在输出中变成交集:

type DynamicParamRoute<T extends string> = (T extends any ? (x:
    `${string}/${DynamicParam<T>}/${string}` | 
    `${DynamicParam<T>}/${string}` | 
    `${string}/${DynamicParam<T>}` | 
    `${DynamicParam<T>}`
) => void : never) extends ((x: infer I) => void) ? I : never;

我已经用(T extends any ? (x: OrigDefinition&lt;T&gt;) =&gt; void : never) extends ((x: infer I) =&gt; void) ? I : never; 包装了您的原始定义。这需要Tdistributes 中的联合,以便OrigDefinition&lt;T&gt; 分别应用于每个部分,并将它们作为函数参数(它们是逆变的)放在inferring 与它们的交集之前。它有点毛茸茸,但最终它会产生你想要的类型:

type RouteParams = DynamicParamRoute<Params>
/* (":user" | `${string}/:user/${string}` | `:user/${string}` | `${string}/:user`) & 
(":team" | `${string}/:team/${string}` | `:team/${string}` | `${string}/:team`) */

这会导致你想要的错误:

const r1: RouteParams = ':user' // error!
const p1: RouteParams = ':team' // error!

我不确定模式模板文字类型的联合交集在实践中的扩展性如何,但这无论如何都超出了问题的范围。

Playground link to code

【讨论】:

  • 感谢 jcalz!我查看了其中几个联合到交叉 stackoverflow 帖子,但无法完全理解它是如何工作的。主要是围绕使用实际上不存在的那种中间函数类型,这真的让我感到困惑。我刚把它插上它,它他妈的很好地钉住了它!你真的是一个TS Guru。
猜你喜欢
  • 2015-10-07
  • 1970-01-01
  • 2010-09-20
  • 2021-08-10
  • 2023-01-03
  • 2022-01-11
  • 1970-01-01
  • 1970-01-01
  • 2021-12-09
相关资源
最近更新 更多