DEV Community

Cover image for Utility Type: WithPrefix
teamradhq
teamradhq

Posted on

Utility Type: WithPrefix

A utility type for prefixing a type's properties with a string:

export type WithPrefix<Prefix extends string, T, Separator extends string = '/'> = {
  [K in keyof T as `${Prefix}${Separator}${string & K}`]: T[K];
};
Enter fullscreen mode Exit fullscreen mode

Simply provide a prefix, type and optional separator (defaults to /).

type UserApi = {
  get: GetFn<User>;
  set: SetFn<User>;
  all: ListFn<User>;
};

const api: WithPrefix<'/api', UserApi> = {
  '/api/get': getUser,
  '/api/set': setUser,
  '/api/all': listUsers,
};
Enter fullscreen mode Exit fullscreen mode

Example: API routes

When handling interactions between a front-end and back-end we might have an API that looks like this:

type CartApi = {
  checkout: CheckoutFn;
  view: GetFn<Cart>;
  add: PostFn<CartItem | CartItem[]>;
  remove: DeleteFn<CartItem | CartItem[]>;
  update: PatchFn<CartItem | CartItem[]>;
};
Enter fullscreen mode Exit fullscreen mode

To ensure that every API method has a corresponding server route we can use the WithPrefix utility type:

const cartApi: WithPrefix<'/cart', CartApi> = {
  '/cart/checkout': checkout,
  '/cart/view': viewCart,
  '/cart/add': addToCart,
  '/cart/remove': removeFromCart,
  '/cart/update': updateCart,
};

for (const [route, handler] of Object.entries(cartApi)) {
  app[handler.method](route, handler);
}
Enter fullscreen mode Exit fullscreen mode

Whenever we define additional methods on our CartApi:

type CartApi = {
  checkout: CheckoutFn;
  view: GetFn<Cart>;
  add: PostFn<CartItem | CartItem[]>;
  remove: DeleteFn<CartItem | CartItem[]>;
  update: PatchFn<CartItem | CartItem[]>;
  empty: DeleteFn<Cart>;
};
Enter fullscreen mode Exit fullscreen mode

TypeScript will show an error:

// TS2741: Property '/cart/empty' is missing in type WithPrefix<'/cart', CartApi>
const cartApi: WithPrefix<'/cart', CartApi> = {
  '/cart/checkout': checkout,
  '/cart/view': viewCart,
  '/cart/add': addToCart,
  '/cart/remove': removeFromCart,
  '/cart/update': updateCart,
};
Enter fullscreen mode Exit fullscreen mode

Breaking it down

This utility uses mapped types, template literal types and key remapping:

export type WithPrefix<Prefix extends string, T, Separator extends string = '/'> = {
  [K in keyof T as `${Prefix}${Separator}${string & K}`]: T[K];
};
Enter fullscreen mode Exit fullscreen mode
  • We define a type with:
    • A Prefix to add to each property.
    • A typeT to prefix.
    • ASeparator to separate the prefix from the property name.
  • We iterate over each property of T and interpolate the prefix and separator with the property name ${Prefix}${Separator}${string & K}.

This gives us a resulting type with our prefixed property names.


Hopefully you find this useful :)


Reference

Top comments (0)