Created
August 15, 2023 18:14
-
-
Save bolencki13/8f975b3728755f61605704cc45e0acfd to your computer and use it in GitHub Desktop.
An example outlining typescript and path parameter injection to strings.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/****************Data model type***************/ | |
interface Endpoint< | |
T_Path extends string = string, | |
> { | |
path: T_Path | |
} | |
// Examples | |
const getUser: Endpoint<'/api/users'> = { | |
path: '/api/users', | |
} | |
const getUserById: Endpoint<'/api/users/:user_id'> = { | |
path: '/api/users/:user_id' | |
} | |
const getProjectsForUserById: Endpoint<'/api/users/:user_id/projects'> = { | |
path: '/api/users/:user_id/projects' | |
} | |
const getProjectByIdForUserById: Endpoint<'/api/users/:user_id/projects/:project_id'> = { | |
path: '/api/users/:user_id/projects/:project_id' | |
} | |
/****************Param parsing***************/ | |
type TrimLeadingChar<T extends string, C extends string> = T extends `${C}${infer Rest}` ? Rest : T; | |
type TrimTrailingChar<T extends string, C extends string> = T extends `${infer Rest}${C}` ? Rest : T; | |
type ParseUrlParams<T_Path extends string, T_Params extends string[] = []> = T_Path extends `${infer T_PathPart}/${infer T_Rest}` | |
? T_PathPart extends `:${infer T_ParamName}` | |
? ParseUrlParams<T_Rest, [...T_Params, T_ParamName]> | |
: ParseUrlParams<T_Rest, T_Params> | |
: T_Params; | |
type InferEndpointParams<T_Endpoint extends Endpoint> = T_Endpoint extends Endpoint<infer T_Path> | |
? ParseUrlParams<`${TrimTrailingChar<TrimLeadingChar<T_Path, '/'>, '/'>}/`> | |
: never; | |
const getParamsFromEndpoint = <T_Endpoint extends Endpoint>(endpoint: T_Endpoint): InferEndpointParams<T_Endpoint> => { | |
if (endpoint.path.startsWith('/')) { | |
return getParamsFromEndpoint({ | |
...endpoint, | |
path: endpoint.path.substring(1) | |
}) | |
} | |
if (endpoint.path.endsWith('/')) { | |
return getParamsFromEndpoint({ | |
...endpoint, | |
path: endpoint.path.substring(0, endpoint.path.length - 1) | |
}) | |
} | |
const endpointParams = endpoint.path.split('/').reduce((params: string[], pathPart) => { | |
if (pathPart.startsWith(':')) { | |
params.push(pathPart.substring(1)) | |
} | |
return params | |
}, []) as InferEndpointParams<T_Endpoint> | |
return endpointParams; | |
} | |
// Examples | |
let getUserParams: InferEndpointParams<typeof getUser>; | |
let getUserByIdParams: InferEndpointParams<typeof getUserById>; | |
let getProjectsForUserByIdParams: InferEndpointParams<typeof getProjectsForUserById> | |
let getProjectByIdForUserByIdParams: InferEndpointParams<typeof getProjectByIdForUserById> | |
getUserParams = getParamsFromEndpoint(getUser); | |
getUserByIdParams = getParamsFromEndpoint(getUserById) | |
getProjectsForUserByIdParams = getParamsFromEndpoint(getProjectsForUserById) | |
getProjectByIdForUserByIdParams = getParamsFromEndpoint(getProjectByIdForUserById) | |
/****************Param Population***************/ | |
type EndpointParams<T_EndpointParams extends string[]> = { | |
[Key in T_EndpointParams[number]]: string | |
} & {} | |
// Examples | |
const getUserParamObject: EndpointParams<typeof getUserParams> = {}; | |
const getUserByIdParamObject: EndpointParams<typeof getUserByIdParams> = { | |
user_id: '1' | |
}; | |
const getProjectsForUserByIdParamsObject: EndpointParams<typeof getProjectsForUserByIdParams> = { | |
user_id: '1' | |
}; | |
const getProjectByIdForUserByIdParamsObject: EndpointParams<typeof getProjectByIdForUserByIdParams> = { | |
user_id: '1', | |
project_id: '2' | |
}; | |
/****************Url generation***************/ | |
type PopulateUrlParams<T_Path extends string, T_Params extends Record<string, string>, T_Url extends string = ''> = T_Path extends `${infer T_PathPart}/${infer T_Rest}` | |
? T_PathPart extends `:${infer T_ParamName}` | |
? PopulateUrlParams<T_Rest, T_Params, `${T_Url}/${T_Params[T_ParamName]}`> | |
: PopulateUrlParams<T_Rest, T_Params, `${T_Url}/${T_PathPart}`> | |
: T_Url; | |
type InferEndpointUrl<T_Endpoint extends Endpoint, T_Params extends EndpointParams<InferEndpointParams<T_Endpoint>> = EndpointParams<InferEndpointParams<T_Endpoint>>> = T_Endpoint extends Endpoint<infer T_Path> | |
? `/${TrimLeadingChar<PopulateUrlParams<`${TrimTrailingChar<TrimLeadingChar<T_Path, '/'>, '/'>}/`, T_Params>, '/'>}` | |
: never; | |
function getUrlForEndpoint <T_Endpoint extends Endpoint, T_Params extends EndpointParams<InferEndpointParams<T_Endpoint>>>(...args: T_Params extends Record<string, never> ? [endpoint: T_Endpoint] : [endpoint: T_Endpoint, params: T_Params]): InferEndpointUrl<T_Endpoint> { | |
const endpoint = args[0]; | |
const params = args[1] ?? {}; | |
let path = endpoint.path; | |
if (path.startsWith('/')) { | |
path = path.substring(1) | |
} | |
if (path.endsWith('/')) { | |
path = path.substring(0, endpoint.path.length - 1) | |
} | |
return endpoint.path.split('/').reduce((url: string[], pathPart) => { | |
if (pathPart.startsWith(':')) { | |
return [...url, params[pathPart.substring(1) as keyof typeof params]] | |
} | |
return [...url, pathPart] | |
}, []).join('/') as InferEndpointUrl<T_Endpoint> | |
} | |
// Examples | |
const getUserUrl = getUrlForEndpoint(getUser); | |
const getUserByIdUrl = getUrlForEndpoint(getUserById, getUserByIdParamObject); | |
const getProjectsForUserByIdUrl = getUrlForEndpoint(getProjectsForUserById, getProjectsForUserByIdParamsObject); | |
const getProjectByIdForUserByIdUrl = getUrlForEndpoint(getProjectByIdForUserById, getProjectByIdForUserByIdParamsObject); | |
/****************Custom Properties***************/ | |
interface Endpoint { | |
meta: { | |
Component: true | |
} | |
} | |
const home: Endpoint<'/'> = { | |
path: '/', | |
meta: { | |
Component: true | |
} | |
} | |
const homeUrl = getUrlForEndpoint(home) | |
const userProfile: Endpoint<'/users/:user_id'> = { | |
path: '/users/:user_id', | |
meta: { | |
Component: true | |
} | |
} | |
const userPorfileUrl = getUrlForEndpoint(userProfile, { | |
user_id: '1' | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment