Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define a global context in a function signature, to enforce typing. #43434

Open
5 tasks done
UrielCh opened this issue Mar 30, 2021 · 3 comments
Open
5 tasks done

Define a global context in a function signature, to enforce typing. #43434

UrielCh opened this issue Mar 30, 2021 · 3 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@UrielCh
Copy link

UrielCh commented Mar 30, 2021

πŸ” Search Terms

  • global context
  • enforce global variable

βœ… Viability Checklist

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

I would like to be able to define that a method can have access to a custom global environment, within the function.
It can be implemented exactly as the this typing.
the main issue is to find a reserved keyword, or syntax to do that.

πŸ“ƒ Motivating Example

Improve puppeteer extensions.

πŸ’» Use Cases

This feature is most useful when we write a code that will be executed in another javascript environment like:

  • writing code that will be injected into a browser (like puppeteer)
  • writing a code that will be called in a nodejs worker.

if I use puppeteer-jquery to enable jQuery into my browser.

I can now write:

import puppeteer from 'puppeteer';
import { PageEx, pageExtend } from 'puppeteer-jquery'

interface Items{ item: string; } 

var jQuery: JQueryStatic; // declare that a jQuery object exists

const browser = puppeteer.launch(option); // start a browser
const page = await browser.newPage(); // create a tab
const page2 = pageExtend(page0); // inject jQuery to the page
const data = (await page.jQuery('div.entry').map((id: number, elm: HTMLElement) => {
    const impData = jQuery(elm).find('.important');
    return { item }
}).pojo()) as Items[];

the jQuery object is available inside the browser but not inside the nodeJS, this code works, but I need to add a dirty var jQuery: JQueryStatic; at the top of the file
currently, the jQuery map method of page signature is:

    map(mapping: (index: number, element: any) => any): PJQueryHybrid;

with a feature to define a custom environment, this code will be replaced by:

import puppeteer from 'puppeteer';
import { PageEx, pageExtend } from 'puppeteer-jquery'

interface Items{ item: string; }

const browser = puppeteer.launch(option); // start a browser
const page = await browser.newPage(); // create a tab
const page2 = pageExtend(page0); // inject jQuery to the page
const data = (await page.jQuery('div.entry').map((id: number, elm: HTMLElement) => {
    const impData = jQuery(elm).find('.important');
    return { item }
}).pojo()) as Items[];

and, the jQuery method of page signature will be something like:

interface JQPage{
    navigator: Navigator;
    jQuery: JQueryStatic;
    // ...
}
// this type is extracted from puppeteer-jquery, it's strange but it is correct.
type PJQueryHybrid = PJQuery & Promise<PJQuery>

interface Page {
    jQuery(global: JQPage, this: unknow, selector: string): PJQuery;
}

interface PJQuery {
    // current
    // map(callback: (this: TElement, index: number, element: TElement) => any): PJQueryHybrid;
    // new version 
    map(callback: (global: JQPage, this: TElement, index: number, element: TElement) => any): PJQueryHybrid;
}

proposition:

The this parameter named is already reserved and must be placed in a first position.
if a global parameter is added before the this parameter it will be interpreted as a global environment, and so will not need reserving a new reserved keyword.

The tsConfig.json can be used to define if the code is used inside a nodeJS or a browser, but when we mixte the two destinations in the same code... it can not be used.

@nmain
Copy link

nmain commented Mar 30, 2021

Possible duplicate of #30750. My search terms were 'worker global'.

@UrielCh
Copy link
Author

UrielCh commented Mar 31, 2021

In this issue, wilsonpage wants to specify a global environment for some of his files, and he works in JS.

I only work in Typescript, and I want my environment to be per function. I think this can also be applied to some web development framework, like in the VueJS composition API, typically the code you are typing will be embedded into a component, and will have access to some new global variable.

With this approach, the new variables are only available in some methods, but not around the method.

in my first code sample, using var jQuery: JQueryStatic;, the jQuery is available everywhere in the file but will fail at runtime if used outside the function block.

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Mar 31, 2021
@rand0me
Copy link

rand0me commented Mar 10, 2025

+1 for this.

There is a common JS pattern, GPT for example:

// Here window.googletag is "uninitialized" and may be typed as { cmd: Function[] }, for example
window.googletag = window.googletag || { cmd: [] };

// This function will be executed after gpt.js has loaded
window.googletag.cmd.push(function () {
  // In this context window.googletag should be "initialized", e.g. it has all other methods/properties:
  window.googletag.defineSlot("/123/some/slot", [300, 250], "banner-ad").addService(googletag.pubads());
  window.googletag.enableServices();
});

The current solution declares only "initialized" state leaving a room for misuse - someone can attempt calling googletag.defineSlot() outside of googletag.cmd.push(...) and get a fancy:

Uncaught TypeError: window.googletag.defineSlot is not a function

With this solution, we could "type guard" it, for example:

declare global {
  interface Window {
    googletag?: GoogleTag
  }
}

// "Uninitialized" googletag
type GoogleTag = {
  cmd: GoogleTagCallback[];
}

// Here's the `global` type override
type GoogleTagCallback = (global: typeof window & { googletag: GoogleTagLoaded }) => void;

// "Initialized" googletag
type GoogleTagLoaded = GoogleTag & {
  defineSlot(/* ... */): void;
  // ...
}

// only `cmd` is visible here, window.googletag is GoogleTag type
window.googletag?.cmd

window.googletag?.cmd.push(function () {
  // Here window.googletag is GoogleTagLoaded type, YAY!
})

I understand adding a reserved parameter name is a big thing, but there's plenty of other libraries working this way - Prebid (prebid.que.push), AppNexus (apntag.anq.push) - things that will be there for decades πŸ˜„

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants