extract param types from string literal
#typescriptbased on this blog post
refresher#
function overload & generics
function firstEl(arr: string[]): string // return string when arr: string[]
function firstEl(arr: number[]): number // return number when arr: number[]
function(arr) { // actual implementation
return arr[0]
}
const string = firstEl(['a', 'b', 'c'])
/*** or ***/
function firstEl<T extends string | number>(arr: T[]): T {
return arr[0]
}
challenge#
how can we get ts to type what's returned in req from just the path string?
app.get('/purchase/[shopId]/[itemId]/args/[...args]', (req) => {
const { params } = req
/**
* ^^^
* const params: {
* shopId: number
* itemId: number
* args: string[]
* }
*/
const { foo } = req.params // ts will complain
})
solution#
split path into union type by '/'
type Parts<Path> = Path extends `${infer PartA}/${infer PartB}`
? PartA | Parts<PartB>
: Path
type ABCD = Parts<'a/b/c/d'> // type ABCD = 'a' | 'b' | 'c' | 'd'
use conditional and never to remove non param parts
type IsParam<Part> = Part extends `[${infer A}]` ? Part : never
type Purchase = IsParam<'purchase'> // type Purchase = never
type ShopId = IsParam<'[shopId]'> // type ShopId = '[shopId]'
combine with prev step
type IsParam<Part> = Part extends `[${infer A}]` ? Part : never
type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}`
? IsParam<PartA> | FilteredParts<PartB>
: IsParam<Path>
type Params = FilteredParts<'/purchase/[shopId]/[itemId]/args/[...args]'>
// type Params = '[shopId]' | '[itemId]' | '[...args]'
remove bracket
type IsParam<Part> = Part extends `[${infer Param}]` ? Param : never
type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}`
? IsParam<PartA> | FilteredParts<PartB>
: IsParam<Path>
type Params = FilteredParts<'/purchase/[shopId]/[itemId]/args/[...args]'>
// type Params = 'shopId' | 'itemId' | '...args'
build Params type
type Params<Path> = {
[Key in FilteredParts<Path>]: unknown
}
type ParamObject = Params<'/purchase/[shopId]/[itemId]/args/[...args]'>
/**
* type Params = {
* shopId: unknown
* itemId: unknown
* '...args': unknown
* }
*/
define map value
type ParamVal<Key> = Key extends `...${A}` ? string[] : number
type ShopId = ParamVal<'shopId'> // type ShopId = number
type Args = ParamVal<'...args'> // type Args = string[]
remove '...'
type RemovePrefix<Key> = Key extends `...${Name}` ? Name : Key
type Args = RemovePrefix<'...args'> // type Args = 'args'
type ShopId = RemovePrefix<'shopId'> // type ShopId = 'shopId'
winner winner chicken dinner
type IsParam<Part> = Part extends `[${infer Param}]` ? Param : never
type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}`
? IsParam<PartA> | FilteredParts<PartB>
: IsParam<Path>
type RemovePrefix<Key> = Key extends `...${Name}` ? Name : Key
type ParamVal<Key> = type ParamVal<Key> = Key extends `...${A}` ? string[] : number
type Params<Path> = {
[Key in FilteredParts<Path> as RemovePrefix<Key>]: ParamVal<Key>
}
type ParamObject = Params<'/purchase/[shopId]/[itemId]/args/[...args]'>
/**
* type ParamObject = {
* shopId: number
* itemId: number
* args: string[]
* }
*/
type CallbackFn<Path> = (req: { params: Params<Path> }) => void
function get<Path extends string>(path: Path, callback: CallbackFn<Path>): void {
// impl.
}