Utility Type: PrependArgs
Tags: typescript, javascript, electron, programming
Published: 2023-11-28 03:12:31


A utility type to prepend a function's arguments:

```typescript
type PrependArgs<ArgsToPrepend extends unknown[], T> = T extends (
  ...args: infer Args
) => infer Returns
  ? (...args: [...ArgsToPrepend, ...Args]) => Returns
  : never;
```

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

```typescript
type UpdateUsername = (username: string) => void;
type WithEvent = PrependArgs<[Event<HTMLInputElement>], UpdateUsername>;
// (event: Event<HTMLInputElement>, username: string) => void;
```

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

```typescript
type PrependPropertyArgs<ArgsToExtend extends unknown[], T> = {
  [K in keyof T]: PrependArgs<ArgsToExtend, T[K]>;
};
```

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

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

type UpdateEventHandlers = PrependPropertyArgs<[Event<HTMLInputElement>], UpdateHandlers>
```
### Example: Handling Electron IPC Events

This example uses the [WithPrefix utility](https://dev.to/teamradhq/utility-type-withprefix-eje) mentioned previously. In case you missed it, it's a utility for prefixing a type's properties with some value:

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

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:

```typescript
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>;
      },
    };
  }
}
```

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

```typescript
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');
  },
};
```

On the main process side we would define corresponding handlers:

```typescript
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();
});
```

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:

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

for (const [channel, handler] of Object.entries(authHandlers)) {
  ipcMain.handle(channel, handler);
}
```

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`:

```typescript
type AuthIpcEvents = PrependPropertyArgs<
  [Electron.IpcMainInvokeEvent],
  WithPrefix<'auth', Window['api']['auth']>
>;
```

- `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:

```typescript 

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>,
};
```

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

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

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

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

### Breaking it down.

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

```typescript
type PrependArgs<ArgsToPrepend extends unknown[], T> = T extends (
  ...args: infer Args
) => infer Returns
  ? (...args: [...ArgsToPrepend, ...Args]) => Returns
  : never;
```
- 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 :)