Skip to content

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.OS branching — 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.ts if exists, else Button.native.ts, else Button.ts.
  • Android → Button.android.ts if exists, else Button.native.ts, else Button.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.OS branching may be simpler and more readable.

Seen in

Last updated · 507 distilled / 1,218 read