Proposal
Flip the layering in one pass: the module becomes the source of truth, and the ambient globals become a thin compat shim over it.
Today types-ios/types-android declare ~10k ObjC/Java names as globals (UIApplication, java.io, android.app, interop…), and @nativescript/core's platform declarations bind them ambiently. Instead:
- Move the generated surface into modules.
types-ios and types-android ship their decls as module exports — export declare class UIView, export import io = java.io, etc. The generator already emits flat files; this is a header/footer rewrite plus export prefixes, not a semantic change.
- Core's platform decls import, not assume.
index.ios.d.ts gets import type { UIView } from '@nativescript/types-ios' for each referenced platform name — mechanical, one import type line per file (the referenced names are greppable).
- Recreate the ambient as an opt-in shim. Keep
types: ["@nativescript/types"] working via a single globals.d.ts:
declare global {
type UIView = import('@nativescript/types-ios').UIView
var UIApplication: typeof import('@nativescript/types-ios').UIApplication
// ...generated for the full surface
}
export {}
Programs with the types entry see the exact same globals as today — zero breaking change. Programs without it get nothing ambient and core still typechecks precisely, because its decls bind the module directly.
That's the whole PR. One mechanical rewrite across types-ios, types-android, and the platform .d.ts emission in core — no phased rollout, no dual maintenance.
Why
- Collisions stop being file-order-dependent. Two ambient
declare class NSObject in one program is first-wins; any package carrying its own ObjC surface (e.g. a hand-maintained AppKit layer for @nativescript/macos-node-api, which ships no generated types) currently fights upstream for the same name and the outcome depends on declaration order the tsconfig author can't fully control.
- Multi-platform programs become possible. Checking iOS + Android + other native code in one
tsc run means one global namespace — module-scoped platform types eliminate the collision class entirely.
- Opt-out stops silently degrading. Today a program without
types: ["@nativescript/types"] doesn't error — skipLibCheck degrades core's platform members (nativeViewProtected: UIView) to any. With module binding, core is precisely typed whether or not the ambient shim is loaded.
- The escape hatch gets visible. Platform-native names become explicit imports — discoverable, greppable, lint-enforceable — instead of unexplained globals.
(Verified: import '@nativescript/core' doesn't pull the ambient — index.d.ts only references global-types.d.ts. The ambient enters exclusively via types://// entries, so the opt-in boundary already exists; this change just makes the module form the canonical one.)
Scope
types-ios, types-android: emit module form + generated globals.d.ts shim.
@nativescript/core: platform .d.ts emission gains import type lines; runtime untouched — the values are runtime globals, only declarations move.
- Docs:
types: ["@nativescript/types"] stays the recommended ambient opt-in; apps that want strict module scoping simply omit it.
Happy to drive the PR — the rewrite is scriptable and the verification is the existing test suite plus a program compiled without the types entry.