Skip to content

Instantly share code, notes, and snippets.

@bolencki13
Created August 15, 2023 18:14
Show Gist options
  • Save bolencki13/8f975b3728755f61605704cc45e0acfd to your computer and use it in GitHub Desktop.
Save bolencki13/8f975b3728755f61605704cc45e0acfd to your computer and use it in GitHub Desktop.
An example outlining typescript and path parameter injection to strings.
/****************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