Repository navigation
Description
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-iosandtypes-androidship 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 plusexportprefixes, not a semantic change. - Core's platform decls import, not assume.
index.ios.d.tsgetsimport type { UIView } from '@nativescript/types-ios'for each referenced platform name — mechanical, oneimport typeline per file (the referenced names are greppable). - Recreate the ambient as an opt-in shim. Keep
types: ["@nativescript/types"]working via a singleglobals.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 NSObjectin 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
tscrun 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 —skipLibCheckdegrades core's platform members (nativeViewProtected: UIView) toany. 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 + generatedglobals.d.tsshim.@nativescript/core: platform.d.tsemission gainsimport typelines; 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.
Activity
NathanWalker commentedon Oct 14, 2026
Great topics here, we'll link in with these - quite a few ideas have been shared on this: NativeScript/rfcs#45