-
Notifications
You must be signed in to change notification settings - Fork 13.2k
Description
Acknowledgement
- I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion.
Comment
Motivation
APIs such as page.evaluate in browser automation libraries (e.g. Playwright) execute a function in a different JavaScript runtime context (browser context instead of Node.js).
Example:
const myText = "Hello world";
await page.evaluate(() => {
console.log(myText); // ❌ Runtime error (myText is not defined in browser)
});Although this fails at runtime, TypeScript does not report an error. The compiler assumes the lambda executes in the same lexical and global environment as the caller.
This creates a mismatch between TypeScript’s static model and the actual runtime semantics of cross-context APIs.
Problem Statement
TypeScript currently has no way to express that:
- A function executes in a separate runtime context
- The function must not capture outer-scope variables
- The function must not access Node.js globals
- The function runs against a specific global type (e.g.
Window)
As a result:
- Accidental closure capture is allowed
- Incorrect global usage is not flagged
- Runtime errors occur that could be prevented at compile time
Real-World Example
const secret = 42;
await page.evaluate(() => {
console.log(secret); // ❌ Runtime error: secret is not defined
});TypeScript currently allows this because it has no notion of execution context isolation.
Proposed Solution
Introduce a way to declare that a function:
- Executes in a separate context
- Has no access to outer lexical scope
- Uses a specified global type
Option A: isolated Function Modifier (New Syntax)
const myText = "Hello world";
await page.evaluate(isolated () => {
console.log(document.title); // ✅ OK
console.log(myText); // ❌ Compile error
});Semantics:
- No closure capture allowed
- No outer-scope variable access
- Global type defined by API signature
- Treated as if compiled in a separate program with structured-clone semantics
This would be conceptually similar to --isolatedModules, but at function scope.
Option B: CrossContext<TGlobal> Type-Level Mechanism
If new syntax is not desirable, introduce a type-level mechanism:
type CrossContext<TGlobal, TArgs extends any[], TResult> =
(this: TGlobal, ...args: TArgs) => TResult;Library usage example:
interface Page {
evaluate<R, A>(
fn: CrossContext<Window, [A], R>,
arg: A
): Promise<R>;
}With compiler rule:
- Functions of type
CrossContext<...>may not reference outer lexical bindings - Only globals available on
TGlobalare allowed
Example behavior:
const x = 5;
await page.evaluate(() => {
console.log(x); // ❌ Error: outer scope capture not allowed
console.log(process); // ❌ Error: not part of Window
console.log(document); // ✅ OK
});Option C: JSDoc Annotation
await page.evaluate(
/** @crossContext Window */
() => {
console.log(document.title);
}
);Compiler behavior:
- Treat function as isolated
- Restrict global access
- Disallow closure capture
This avoids syntax changes while still enabling static safety.
Prior Art
- Web Workers (structured cloning, no shared closures)
postMessagesemantics- Rust’s
Send/Syncclosure restrictions - Sandboxed runtimes (e.g. Electron multi-process model)
Benefits
- Prevents a common class of runtime errors
- Improves safety of browser automation, workers, and SSR
- Makes execution semantics explicit
- Improves editor tooling and developer experience
- Enables safer multi-runtime APIs
Non-Goals
- Not intended as a security boundary
- Not intended to restrict normal functions
- Not a replacement for ESLint, but a stronger compiler guarantee
Why This Belongs in TypeScript (Not Just ESLint)
While ESLint could detect some closure capture cases, only the TypeScript compiler can:
- Properly reason about type environments
- Enforce global type restrictions
- Integrate deeply with existing type inference
Cross-context execution is increasingly common in:
- Browser automation
- Workers
- Hybrid runtimes
- SSR frameworks
TypeScript currently has no way to model this distinction.
Summary
TypeScript lacks a mechanism to model execution-context isolation. Introducing an isolated function modifier or a CrossContext<TGlobal> constraint would prevent runtime errors and better reflect modern JavaScript runtime architectures.