Lusito / webextension-polyfill-ts

This is a TypeScript ready "wrapper" for the WebExtension browser API Polyfill by Mozilla
zlib License
392 stars 23 forks source link

How to properly extends types using declarations file #93

Closed OlegWock closed 1 year ago

OlegWock commented 1 year ago

Hello! I'm currently using declaration files to add missing types or correct some of existing ones. This works fairly well, but I couldn't properly add 'root-level' api (like browser.tabGroups) using this approach. Do you maybe know how to do it properly? This is how I tried to do it, but it didn't work:

import 'webextension-polyfill';

declare module 'webextension-polyfill' {
    // Extending existing APIs works fine
    namespace Tabs {
        interface Static {
            group: (options: {createProperties?: {windowId?: number}, groupId?: number, tabIds?: number | number[]}) => Promise<number>
        }
    }

    // Adding new API doesn't work :( 
    namespace Browser {
        type TabGroupColor = "grey" | "blue" | "red" | "yellow" | "green" | "pink" | "purple" | "cyan" | "orange";
        interface TabGroup {
            collapsed: boolean,
            color: TabGroupColor,
            id: number,
            windowId: number,
            title?: string,
        }

        interface TabGroupsStatic {
            update: (groupId: number, updateProperties: {collapsed?: boolean, color?: TabGroupColor, title?: string}, ) => Promise<TabGroup | undefined>
        }

        const tabGroups: TabGroupsStatic;

        interface Browser {
            tabGroups: TabGroupsStatic
        }
    }
}
Lusito commented 1 year ago

Hi,

Not sure why this does not work on root level.. possibly because of the namespace & interface combination.

Since you are using a chrome-only API, maybe it would be an option make it chrome-specific?

Let's say you start with the following:

type TabGroupColor = "grey" | "blue" | "red" | "yellow" | "green" | "pink" | "purple" | "cyan" | "orange";
interface TabGroup {
    collapsed: boolean;
    color: TabGroupColor;
    id: number;
    windowId: number;
    title?: string;
}

interface ChromeTabGroupsStatic {
    update: (
        groupId: number,
        updateProperties: { collapsed?: boolean; color?: TabGroupColor; title?: string }
    ) => Promise<TabGroup | undefined>;
}

interface ChromeTabsStatic {
    group: (options: {
        createProperties?: { windowId?: number };
        groupId?: number;
        tabIds?: number | number[];
    }) => Promise<number>;
}

type ChromeBrowserAdditions = {
    tabGroups: ChromeTabGroupsStatic;
    tabs: ChromeTabsStatic;
};

You now have 3 possibilities to chose from:

Option 1: isChromeBrowser check (if you want to stay cross-browser compatible):

function isChromeBrowser<T extends Browser>(browser: T): browser is T & ChromeBrowserAdditions {
    return "tabGroups" in browser;
}

if (isChromeBrowser(browser)) {
    browser.tabGroups.update(0, {});
}

Now everywhere you use chrome-specific code, you can easily do so by first checking for chrome.

Option 2: A new constant (if you want to use the polyfill export, but no browser check is wanted):

const chrome = browser as Browser & ChromeBrowserAdditions;
chrome.tabGroups.update(0, {});

Option 3: declaring global (if you only care about the types from this package)

declare global {
    const chrome: Browser & ChromeBrowserAdditions;
}

chrome.tabGroups.update(0, {});

Hope this helps

OlegWock commented 1 year ago

First option will be the best, thanks you very much!