PATTERN Cited by 1 source
Platform-specific TS file resolution¶
Problem¶
In a cross-platform UI component library, some components need different implementations per target platform (iOS, Android, web). Naive approaches:
- Runtime
Platform.OSbranching — ships all platform code to every platform; bundle bloat + unnecessary native deps on web. - Duplicate components with different names (e.g.
ButtonWeb,ButtonNative) — leaks the platform split into every consumer; violates the "consumer shouldn't care" invariant. - Build-flag-conditional imports — requires per-consumer bundler config.
You want: one import path per component, resolved to per-platform implementations at build time, with types enforced across implementations.
Pattern¶
Use the bundler's platform-specific file resolution. In the React Native ecosystem, this is provided by Metro — see concepts/platform-specific-import-resolution.
Structure:
Button/
├── index.ts # exports (re-export from the resolved file)
├── Button.types.ts # shared types
├── Button.ts # fallback (web) implementation
├── Button.native.ts # iOS + Android implementation
├── Button.ios.ts # iOS-only override (if needed)
└── Button.android.ts # Android-only override (if needed)
Consumer writes import { Button } from "./Button". Metro
picks the right file based on target:
- iOS →
Button.ios.tsif exists, elseButton.native.ts, elseButton.ts. - Android →
Button.android.tsif exists, elseButton.native.ts, elseButton.ts. - Web →
Button.ts.
Key discipline: separate types file¶
The types file (Button.types.ts) is the contract that
every implementation must satisfy. By pulling types out:
- TypeScript checks every implementation against the same interface — catches drift at compile time.
- Consumers can import types without triggering a platform decision.
- Changes to the contract cascade through the type system to all implementations.
Zalando calls this out: "we started using a simple pattern where types would live in a separate file so that we can have safe type checking between multiple implementations." (Source: sources/2025-10-02-zalando-accelerating-mobile-app-development-with-rendering-engine-and-react-native)
Why this works¶
- Zero runtime dispatch cost. All branching happens at build time.
- Clean bundle boundaries. Web never sees native code; iOS never sees web-specific code.
- Type-safe across platforms. The shared types file is the source of truth.
- Consumer API stability. Regardless of which platforms the component supports or needs to diverge on, the consumer's import stays the same.
When to reach for this pattern¶
- Cross-platform component library where some components must have platform-specific implementations.
- Primary cross-platform substrate (e.g. react-strict-dom + HTML subset) can't cover certain components.
- You want the split invisible to consumers.
When not to¶
- No meaningful platform divergence — just write one file.
- Divergence is small (one or two branches) —
Platform.OSbranching may be simpler and more readable.
Seen in¶
- sources/2025-10-02-zalando-accelerating-mobile-app-development-with-rendering-engine-and-react-native — canonical wiki first source. Zalando's cross-platform design-system component library uses this pattern for components where the react-strict-dom HTML subset isn't expressive enough.