Open
Description
Feature Request
Description
Right now plugins make themselves available by augmenting the PluginRegistry
interface in the @capacitor/core
module, for instance like this:
// definitions.d.ts
declare module '@capacitor/core' {
interface PluginRegistry {
Foo: FooPlugin;
}
}
export interface FooPlugin {
getFoo(): Promise<'foo'>;
getBar(): Promise<'bar'>;
getCurrentPlatform(): Promise<'android' | 'ios' | 'web'>;
}
The plugin is included in the registry unconditionally. When a plugin provides uniform implementations for all three supported platforms, this is not an issue.
However, there are plugins which are not available on all platforms or have varying interfaces per platform. For instance, the above could be expressed in a different way, if the registries for each platforms were disjunct.
// definitions.d.ts
declare module '@capacitor/core' {
interface PluginRegistryAndroid {
Foo: FooPluginAndroid;
}
interface PluginRegistryIOS {
Foo: FooPluginIOS
}
interface PluginRegistryWeb {
Foo: FooPluginWeb;
}
}
export interface FooPlugin {
getFoo(): Promise<'foo'>;
getBar(): Promise<'bar'>;
getCurrentPlatform(): Promise<'android' | 'ios' | 'web'>;
}
export interface FooPluginAndroid extends FooPlugin {
getCurrentPlatform(): Promise<'android'>;
}
export interface FooPluginIOS extends FooPlugin {
getCurrentPlatform(): Promise<'ios'>;
}
export interface FooPluginWeb extends FooPlugin {
getCurrentPlatform(): Promise<'web'>;
}
Through some nifty type for Capacitor.Plugins
we could then achieve a stricter, safer usage, like this:
import { Plugins } from "@capacitor/core";
declare module "@capacitor/core" {
interface PluginRegistry {
LegacyPlugin: { name: "LegacyPlugin" };
}
interface PluginRegistryAndroid {
AndroidPlugin: { name: "AndroidPlugin" };
NativePlugin: { name: "NativePlugin"; platform: "android" };
}
interface PluginRegistryIOS {
IOSPlugin: { name: "IOSPlugin" };
NativePlugin: { name: "NativePlugin"; platform: "ios" };
}
interface PluginRegistryWeb {
WebPlugin: { name: "WebPlugin" };
}
}
// Plugins registered in the legacy `PluginRegistry` are always optional, as
// there is no way to guarantee that they are available. To make them
// accessible without `?.` the plugin author must migrate them to one or more of
// the new registries
Plugins?.LegacyPlugin; // undefined | { name: "LegacyPlugin" }
// Without narrowing the type down via one of the `.is*` platform
// discriminators, all plugins are available as optional, meaning you have to
// use `?.` when accessing them.
Plugins?.AndroidPlugin; // undefined | { name: "AndroidPlugin" }
Plugins?.IOSPlugin; // undefined | { name: "IOSPlugin" }
Plugins?.NativePlugin; // undefined | { name: "NativePlugin"; platform: "android" | "ios" }
Plugins?.WebPlugin; // undefined | { name: "WebPlugin" }
if (Plugins.isAndroid) {
Plugins.isIOS; // false
Plugins.isWeb; // false
// Android-only plugin is available.
Plugins.AndroidPlugin; // { name: "AndroidPlugin" }
// Common native plugin is available and narrowed down to the Android flavor.
Plugins.NativePlugin; // { name: "NativePlugin"; platform: "android" };
// As before, plugins registered in the legacy `PluginRegistry`, are always
// optional and you have to use `?.`.
Plugins.LegacyPlugin; // undefined | { name: "LegacyPlugin" }
// Plugins that are specific to a platform are not accessible.
Plugins.IOSPlugin; // undefined
Plug
— buschtoens/capacitor-plugin-registry/blob/main/demo.ts
Platform(s)
Android, iOS, Web
Preferred Solution
import type { Merge } from "type-fest";
interface PlatformDiscriminator {
isAndroid: boolean;
isIOS: boolean;
isWeb: boolean;
}
// Original legacy registry remains, so that legacy addons can still register themselves.
interface PluginRegistry {}
interface PluginRegistryAndroid {
isAndroid: true;
isIOS: false;
isWeb: false;
}
interface PluginRegistryIOS {
isAndroid: false;
isIOS: true;
isWeb: false;
}
interface PluginRegistryWeb {
isAndroid: false;
isIOS: false;
isWeb: true;
}
// -------------------- Private helper types ------------------------
// These are not meant to be augmented directly by plugins.
// To prevent this, they may be moved to extra files.
type AllPlugins = Partial<
Merge<
PluginRegistry,
Merge<PluginRegistryAndroid, Merge<PluginRegistryIOS, PluginRegistryWeb>>
>
> &
PlatformDiscriminator;
type NoPlugins = Record<
| keyof PluginRegistryAndroid
| keyof PluginRegistryIOS
| keyof PluginRegistryWeb,
undefined
>;
type CapacitorPluginRegistry =
| AllPlugins
| (Partial<PluginRegistry> & Merge<NoPlugins, PluginRegistryAndroid>)
| (Partial<PluginRegistry> & Merge<NoPlugins, PluginRegistryIOS>)
| (Partial<PluginRegistry> & Merge<NoPlugins, PluginRegistryWeb>);
// This is `Capacitor.Plugins`
export const Plugins: CapacitorPluginRegistry;