DEV Community

Cover image for Utility Type: PrependArgs
teamradhq
teamradhq

Posted on

Utility Type: PrependArgs

A utility type to prepend a function's arguments:

type PrependArgs<ArgsToPrepend extends unknown[], T> = T extends (
  ...args: infer Args
) => infer Returns
  ? (...args: [...ArgsToPrepend, ...Args]) => Returns
  : never;
Enter fullscreen mode Exit fullscreen mode

Simply provide an array of arguments to prepend, and the type of the function to extend:

type UpdateUsername = (username: string) => void;
type WithEvent = PrependArgs<[Event<HTMLInputElement>], UpdateUsername>;
// (event: Event<HTMLInputElement>, username: string) => void;
Enter fullscreen mode Exit fullscreen mode

Additionally, we can define a convenience utility to prepend all function arguments on properties of a type:

type PrependPropertyArgs<ArgsToExtend extends unknown[], T> = {
  [K in keyof T]: PrependArgs<ArgsToExtend, T[K]>;
};
Enter fullscreen mode Exit fullscreen mode

This allows us to prepend all arguments on each of type's properties:

type UpdateHandlers = {
  username: (username: string) => void;
  email: (email: string) => void;
  password: (password: string) => void;
};

type UpdateEventHandlers = PrependPropertyArgs<[Event<HTMLInputElement>], UpdateHandlers>
Enter fullscreen mode Exit fullscreen mode

Example: Handling Electron IPC Events

This example uses the WithPrefix utility mentioned previously. In case you missed it, it's a utility for prefixing a type's properties with some value:

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

Electron apps utilise inter-process communication (IPC) to pass information between main processes and render processes. Typically, an API is defined on the global window that can be used to handle incoming events and initiate outgoing events:

declare global {
  interface Window {
    electron: ElectronAPI;
    api: {
      auth: {
        login: (payload: LoginPayload) => Promise<LoginResponse>;
        logout: () => Promise<boolean>;
        isLoggedIn: () => Promise<boolean>;
      },
      preferences: {
        get: () => Promise<Preferences>;
        set: (payload: Partial<Preferences>) => Promise<Preferences>;
      },
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Each of the API methods will initiate some kind of event that is sent to the main process:

export const auth: Window['api']['auth'] = {
  async login(payload) {
    return await window.electron.invoke('auth/login', payload);
  },
  async logout() {
    return await window.electron.invoke('auth/logout', payload);
  },
  async isLoggedIn() {
    return await window.electron.invoke('auth/isLoggedIn');
  },
};
Enter fullscreen mode Exit fullscreen mode

On the main process side we would define corresponding handlers:

ipcMain.handle('auth/login', async (event: Electron.IpcMainInvokeEvent, payload: LoginPayload) => {
  return await loginUser(payload);
});

ipcMain.handle('auth/logout', async (event: Electron.IpcMainInvokeEvent) => {
  return await logout();
});

ipcMain.handle('auth/isLoggedIn', async (event: Electron.IpcMainInvokeEvent) => {
  return await isLoggedIn();
});
Enter fullscreen mode Exit fullscreen mode

Typically, we wouldn't want to call our handlers like this. Instead, we would list them somehow and iterate over them to register each handler:

const authHandlers = {
  'auth/login': handleAuthLogin,
  'auth/logout': handleAuthLogout,
  'auth/isLoggedIn': handleAuthIsLoggedIn,
};

for (const [channel, handler] of Object.entries(authHandlers)) {
  ipcMain.handle(channel, handler);
}
Enter fullscreen mode Exit fullscreen mode

This is a nice way to DRY things up. But there are some issues with this approach:

  • We have no way of knowing whether our implementation covers all of the API methods.
  • We have to define types for each handler with the only difference being that we prepend an Electron.IpcMainInvokeEvent argument.

We can solve both of these issues with a combination of WithPrefix and PrependPropertyArgs:

type AuthIpcEvents = PrependPropertyArgs<
  [Electron.IpcMainInvokeEvent],
  WithPrefix<'auth', Window['api']['auth']>
>;
Enter fullscreen mode Exit fullscreen mode
  • WithPrefix<'auth', Window['api']['auth']> prefixes our types
  • PrependPropertyArgs<[Electron.IpcMainInvokeEvent], T> adds the event parameter as the first argument to each method.

This results in the following type:


type AuthIpcHandlers = {
  'auth/login': (event: Electron.IpcMainInvokeEvent, payload: LoginPayload) => Promise<LoginResponse>;
  'auth/logout': (event: Electron.IpcMainInvokeEvent) => Promise<boolean>,
  'auth/isLoggedIn': (event: Electron.IpcMainInvokeEvent) => Promise<boolean>,
};
Enter fullscreen mode Exit fullscreen mode

Now, if we added an api.auth.register method, we would get a type error:

// TS2741: Property 'auth/register' is missing in type AuthIpcHandlers
const authHandlers = {
  'auth/login': handleAuthLogin,
  'auth/logout': handleAuthLogout,
  'auth/isLoggedIn': handleAuthIsLoggedIn,
};
Enter fullscreen mode Exit fullscreen mode

And our handlers will warn us when we don't provide the event and required arguments:

// TS2554: Expected 2 arguments, but got 1.
const handleLogin: AuthIpcHandlers['auth/login'] = async (payload) => {};
Enter fullscreen mode Exit fullscreen mode

Breaking it down.

This one is relatively simple. We are just defining a new function with our prepended arguments:

type PrependArgs<ArgsToPrepend extends unknown[], T> = T extends (
  ...args: infer Args
) => infer Returns
  ? (...args: [...ArgsToPrepend, ...Args]) => Returns
  : never;
Enter fullscreen mode Exit fullscreen mode
  • We define generic type ArgsToPrepend extends unknown[] so that we can accept any type.
  • We state that T must be a function type T extends (...args: infer Args) => infer Returns and also infer the type of its Args and Returns.
  • We combine our ArgsToPrepend with the inferred Args, resulting in a new function type: (...args: [...ArgsToPrepend, ...Args]) => Returns.
  • Finally, if we have passed a non-function type, we return never.

Hopefully you find this useful :)

Top comments (0)