I'm trying to harness the power of string template literal types to add type safety to the string used to define a route.
- For instance, a route with no parameters can be any
string
. - A route with a single parameter must include
:parameterName
in the path string AND includeparameterName
as a key/value in theparams
object.
I can manually set up these types and it works beautifully. But what I'd like to do is find a way to remove the need for the developer to manually chain the intersection. I'd like to handle that in my library.
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'
I tried an approach using the keyof for a params
object, but it creates the union of the keys. The route definition requires params object, especially if there are dynamic params. So using its keys seems almost too perfect an approach to generate the type required for the path
of the route.
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'
Thanks in advance for any and all insight!
UPDATE: jcalz solution demonstrated as a GIF (I hope it isn't too compressed)
[