A TypeScript-first decorator library that simplifies Electron IPC communication with type safety and automatic proxy generation.
- π― Type-Safe: Full TypeScript support with automatic type inference
- π Decorator-Based: Clean and intuitive API using decorators
- π Auto Proxy: Automatic client-side proxy generation
- π¦ Service Groups: Organize IPC methods into logical service groups
- π‘οΈ Error Handling: Built-in error handling and logging
- β‘ Async Support: Native support for async/await patterns
- π Context Injection: AsyncLocalStorage-based context management
npm install electron-ipc-decorator # or pnpm add electron-ipc-decorator # or yarn add electron-ipc-decorator
import { app } from 'electron' import { IpcService, IpcMethod, getIpcContext } from 'electron-ipc-decorator' export class AppService extends IpcService { static readonly groupName = 'app' // Define static group name @IpcMethod() getAppVersion(): string { return app.getVersion() } @IpcMethod() switchAppLocale(locale: string): void { i18n.changeLanguage(locale) app.commandLine.appendSwitch('lang', locale) } @IpcMethod() async search(input: SearchInput): Promise<Electron.Result | null> { // Context is automatically injected via AsyncLocalStorage // Access it using getIpcContext() when needed // Get context when needed const { sender: webContents } = getIpcContext() const { promise, resolve } = Promise.withResolvers<Electron.Result | null>() let requestId = -1 webContents.once('found-in-page', (_, result) => { resolve(result.requestId === requestId ? result : null) }) requestId = webContents.findInPage(input.text, input.options) return promise } }
import { createServices, MergeIpcService } from 'electron-ipc-decorator' import { AppService } from './app-service' // Create services with automatic type inference export const services = createServices([AppService]) // Generate type definition for all services export type IpcServices = MergeIpcService<typeof services>
import { createIpcProxy } from 'electron-ipc-decorator/client' import type { IpcServices } from './main/services' // Import from main process // ipcRenderer should be exposed through electron's context bridge export const ipcServices = createIpcProxy<IpcServices>(ipcRenderer)
// Synchronous methods const version = await ipcServices.app.getAppVersion() // Methods with parameters await ipcServices.app.switchAppLocale('en') // Async methods const searchResult = await ipcServices.app.search({ text: 'search term', options: { findNext: false, forward: true, }, })
Marks a method as an IPC endpoint.
@IpcMethod() someMethod() { }
Base class for creating IPC service groups.
abstract class IpcService { static readonly groupName: string // Must be defined by subclasses }
Context object available through getIpcContext() within IPC method handlers. The context is automatically injected using AsyncLocalStorage.
interface IpcContext { sender: WebContents // The WebContents that sent the request event: IpcMainInvokeEvent // The original IPC event }
Retrieves the current IPC context from AsyncLocalStorage. Must be called within an IPC method handler.
function getIpcContext(): IpcContext
Creates services from an array of service constructors with automatic type inference. Each service class must define a static groupName property.
// Define services class AppService extends IpcService { static readonly groupName = 'app' // methods... } class UserService extends IpcService { static readonly groupName = 'user' // methods... } // Create services with type safety const services = createServices([AppService, UserService]) // Type is: { app: AppService, user: UserService }
Creates a type-safe proxy for calling IPC methods from the renderer process.
Merges multiple service instances into a single type definition.
Extracts and transforms service methods for client-side usage, automatically:
- Preserves all method parameters
- Wraps return types in
Promise<T>
- Context Access: Access IPC context using
getIpcContext()within method handlers when needed - Return Types: All methods return
Promise<T>on the client side, even if they're synchronous on the server - Error Handling: Errors are automatically propagated from main to renderer process
// Main process method signature @IpcMethod() someMethod(param1: string, param2: number): string { const context = getIpcContext() // Get context when needed // Implementation } // Renderer process usage (auto-generated type) ipcServices.group.someMethod(param1: string, param2: number): Promise<string>
Version 1.0.0 introduces a breaking change in how IPC context is accessed. The context parameter has been removed from all IPC method signatures in favor of AsyncLocalStorage-based context injection.
Before (v0.x):
import { IpcService, IpcMethod, IpcContext } from 'electron-ipc-decorator' export class AppService extends IpcService { static readonly groupName = 'app' @IpcMethod() someMethod(context: IpcContext, param: string): string { const { sender } = context // Use context... return 'result' } }
After (v1.0.0):
import { IpcService, IpcMethod, getIpcContext } from 'electron-ipc-decorator' export class AppService extends IpcService { static readonly groupName = 'app' @IpcMethod() someMethod(param: string): string { const { sender } = getIpcContext() // Get context when needed // Use context... return 'result' } }
- Cleaner API: Method signatures match their actual purpose without boilerplate
- On-Demand Context: Only retrieve context when you actually need it
- Better Type Inference: Automatic type extraction works more accurately
- Promise Chain Context: Context is preserved throughout async operations via AsyncLocalStorage
- Remove
IpcContextparameter from all@IpcMethod()decorated methods - Import and use
getIpcContext()when you need to access the context - Update your type definitions (the types will now automatically match your method signatures)
- No changes needed on the renderer side - the API remains the same
If you're upgrading from a version that used constructor-based group names, here's how to migrate:
Old way:
export class AppService extends IpcService { constructor() { super('app') // Group name in constructor } } // Manual service object creation export const services = { app: new AppService(), }
New way (recommended):
export class AppService extends IpcService { static readonly groupName = 'app' // Static group name } // Automatic service creation with type safety export const services = createServices([AppService])
Benefits of the new approach:
- Type Safety: Prevents mismatch between service keys and group names
- Auto-completion: Full IntelliSense support for service methods
- Runtime Safety: Compile-time errors if
groupNameis missing - Simpler: No need to manually maintain service object keys
Errors thrown in main process methods are automatically caught and re-thrown in the renderer process:
@IpcMethod() riskyOperation(): string { throw new Error("Something went wrong") } // In renderer try { await ipcServices.app.riskyOperation() } catch (error) { console.error("IPC Error:", error.message) // "Something went wrong" }
@IpcMethod() sendNotification(message: string): void { const { sender } = getIpcContext() // Send data back to the specific renderer sender.send('notification', { message, timestamp: Date.now() }) }
This project maintains 100% test coverage. To run tests:
# Run tests pnpm test # Run tests with coverage pnpm test:coverage # Run tests in watch mode pnpm test:watch
Ensure your tsconfig.json has decorator support enabled:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}2025 Β© Innei, Released under the MIT License.
Personal Website Β· GitHub @Innei