首页 > 解决方案 > 定义 1...n 个字符串模板类型的通用交集类型

问题描述

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

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

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对象的方法,但它创建了键的联合。路由定义需要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(我希望它不会太压缩)

[使用 jcalz 方法的有效解决方案1

标签: typescript

解决方案


可以通过某种类型的杂耍将联合转换为 TypeScript 中的交集,从而将所讨论的联合置于逆变位置。这是我重新定义的方式DynamicParamRoute<T>,以便联合在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<T>) => void : never) extends ((x: infer I) => void) ? I : never;. 这将联合T分配它们,以便OrigDefinition<T>分别应用于每个部分,并将它们作为函数参数(它们是逆变的),然后再从它们推断交集。它有点毛茸茸,但最终它会产生你想要的类型:

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 代码链接


推荐阅读