In a TypeScript React Native project with only DatePicker.ios.tsx and DatePicker.android.tsx, why does import './DatePicker' fail type-checking, and how is it fixed?
answer
- tsc resolves without Metro
- no suffix knowledge by default
- a shared DatePicker.d.ts
- moduleSuffixes with a trailing ''
- one platform per tsconfig
basics
~20 sTypeScript resolves modules itself and does not know Metro's platform suffixes, so no DatePicker file exists for it. Add a DatePicker.d.ts declaring the shared API, or set moduleSuffixes such as ['.ios', '.native', ''] per platform tsconfig.
solid answer
~40 sThe TypeScript compiler does its own module resolution and by default knows nothing about Metro's `.ios` and `.android` convention; React Native's `@react-native/typescript-config` does not add it. So `./DatePicker` has no match and `tsc` reports that it cannot find the module. There are two standard fixes. A **declaration file**, `DatePicker.d.ts`, next to the implementations declares the component and its props; TypeScript resolves the import to it and Metro never bundles it. Both implementations should import a shared `DatePickerProps` type, because nothing checks them against the `.d.ts`. Alternatively, the **`moduleSuffixes`** option, for example `[".ios", ".native", ""]`, makes TypeScript try suffixed files first; the empty string keeps ordinary imports working. It resolves one platform per config, so a second config with `.android` checks the Android side.
code
typescript · 13 lines// DatePicker.types.ts
export type DatePickerProps = {
value: Date;
minimumDate?: Date;
onChange: (date: Date) => void;
};
// DatePicker.d.ts
import type { ReactElement } from 'react';
import type { DatePickerProps } from './DatePicker.types';
export type { DatePickerProps };
export default function DatePicker(props: DatePickerProps): ReactElement;go deeper
Recall that TypeScript and Metro resolve imports separately, so a split module needs extra help to type-check, usually a .d.ts or moduleSuffixes.
Explain both fixes: a declaration file plus a shared props type, or moduleSuffixes with the trailing empty string, and why each resolves only what it describes.
Choose per codebase: declaration files for a few split modules, moduleSuffixes with a type-check per platform in CI for many, and never silence the error with ts-ignore.
Make per-platform type-checking part of the build contract, so no platform's code can compile only in the editor of the developer who wrote it.
## Two resolvers, two rule sets A React Native project has two independent programs that resolve imports: - **Metro**, the bundler, which knows about platforms and tries `DatePicker.ios.tsx`, then `DatePicker.native.tsx`, then `DatePicker.tsx` when bundling for iOS; - **`tsc`**, the TypeScript compiler, which only type-checks. Its default resolution looks for `DatePicker.ts`, `DatePicker.tsx`, `DatePicker.d.ts` and similar, with no notion of a platform. React Native's shared TypeScript config, `@react-native/typescript-config`, sets `moduleResolution: "bundler"`, `noEmit` and the `react-native` custom condition, but no platform suffixes. So in a clinic-booking app with only `DatePicker.ios.tsx` and `DatePicker.android.tsx`, Metro builds happily while `tsc` and the editor report that `./DatePicker` cannot be found. The build working and the type-check failing is the tell. ## Option 1: a shared declaration file 1. Put the contract in `DatePicker.types.ts`: `DatePickerProps` with `value`, `minimumDate` and `onChange`. 2. Make both implementations import that type and annotate their props with it, so each one is checked against the contract. 3. Add `DatePicker.d.ts` beside them, declaring the default export's type and re-exporting the props type. TypeScript resolves `./DatePicker` to the `.d.ts` file. Metro never picks it up, because its candidates are `DatePicker` plus a platform and a source extension, and `DatePicker.d.ts` is none of them. Two cautions: the `.d.ts` is a **promise** that TypeScript does not verify against the implementations, which is why step 2 matters; and go-to-definition in the editor lands on the declaration, not the code. ## Option 2: moduleSuffixes TypeScript's `moduleSuffixes` compiler option lists suffixes to try, in order, before the extension: - `[".ios", ".native", ""]` makes `./DatePicker` resolve to `DatePicker.ios.tsx` if it exists, then `DatePicker.native.tsx`, then `DatePicker.tsx`. - The **empty string is essential**. Without it, every ordinary import of an unsuffixed file stops resolving. - It describes **one platform**. With the iOS list, every import resolves to the iOS variant; the Android files are still compiled if included, but no call site is checked against them. A second config, say `tsconfig.android.json` extending the base with `[".android", ".native", ""]`, and a second `tsc -p` run cover Android. ## Wiring it into CI Whichever option you choose, make the type-check an explicit step rather than trusting the editor: 1. keep the base `tsconfig.json` as the developer default, so the editor resolves imports one consistent way; 2. with `moduleSuffixes`, add one small config per platform that only overrides the suffix list, and run `tsc -p` against each; 3. with declaration files, run one `tsc` over the whole project, which checks both implementation files against the shared props type; 4. fail the pipeline on any error, since Metro will happily bundle code that does not type-check. The last point is the one teams forget: Metro transforms TypeScript without type-checking it, so a prop mismatch in the Android picker builds, installs and fails only when a patient taps the field. ## Comparing the options | | Shared `.d.ts` | `moduleSuffixes` | |---|---|---| | Extra files | one declaration per split module | none | | Call sites checked against | the declared contract | the real implementation of one platform | | Checks both platforms | contract only, via shared props type | yes, with one config per platform | | Editor navigation | lands on the declaration | lands on one implementation | | Cost | keep the `.d.ts` in sync | two type-check runs in CI | Small projects with a handful of split modules often prefer the declaration file. Projects with many split modules, or where the implementations export extra helpers, often prefer `moduleSuffixes` with a type-check per platform. ## Pitfalls - Adding `// @ts-ignore` or a wildcard `declare module` to silence the error, which removes all checking of the date picker's props at every call site. - Writing `moduleSuffixes` without the trailing `""`. - Keeping both approaches for the same module. Once `moduleSuffixes` finds `DatePicker.ios.tsx`, call sites are checked against that file rather than the declaration, so the `.d.ts` quietly goes stale while still looking authoritative. Pick one approach per project and delete the leftovers. - Running only the iOS type-check in CI, so an Android-only prop mismatch ships. - Letting the implementations export different named helpers, which the `.d.ts` cannot describe for both; keep the public surface identical and move extras into shared files.
- Why must a React Native project's moduleSuffixes list end with an empty string?`moduleSuffixes` replaces TypeScript's default of trying no suffix. With only `.ios` and `.native` listed, an import of `./utils` would look for `utils.ios.ts` and `utils.native.ts` and never plain `utils.ts`, so almost every ordinary import in the project would fail to resolve. The empty string restores the unsuffixed lookup as the last attempt.
- With a DatePicker.d.ts in place, how do you stop the Android implementation from drifting from the declared props?Nothing compares the implementations with the declaration, so make them depend on the same type: both files import `DatePickerProps` from a shared types module and annotate their props with it. A prop added to the contract then fails to compile in whichever implementation ignores it, provided both files are included in the type-check.
saying these in an interview costs you the question
- If Metro can bundle it, tsc will resolve the same import.
- React Native's TypeScript config already understands .ios and .android suffixes.
- moduleSuffixes ['.ios', '.android'] type-checks both platforms at once.
- TypeScript verifies that each implementation matches the .d.ts.
- Metro bundles DatePicker.d.ts as the fallback implementation.