In a React Native TypeScript app, what turns .tsx into JavaScript for Metro, and why can the app run with type errors?
answer
- two tools, two jobs
- @react-native/babel-preset strips types
- Metro never runs the type checker
- noEmit in the base tsconfig
- isolatedModules keeps Babel safe
basics
~10 sMetro compiles .ts and .tsx with Babel, where @react-native/babel-preset's TypeScript plugin deletes type annotations without checking them. Type errors are only reported by tsc, which @react-native/typescript-config runs as a noEmit checker.
solid answer
~40 sReact Native splits TypeScript into two jobs. **Transpiling** is Babel's: when Metro bundles, `@react-native/babel-preset` applies `@babel/plugin-transform-typescript` to `.ts` and `.tsx` files, which removes the types file by file and never looks at whether they are correct. **Checking** is `tsc`'s: the base `@react-native/typescript-config` sets `noEmit: true`, so `tsc` only type-checks and writes nothing. That is why a type error never shows up as a red screen and the app still runs, while a syntax error does stop the bundle. The config also sets `isolatedModules: true`, so `tsc` flags code Babel cannot compile one file at a time. In practice the editor, a pre-commit hook and CI run `tsc`, because Metro never will.
code
javascript · 4 lines// babel.config.js
module.exports = {
presets: ['module:@react-native/babel-preset'],
};go deeper
Recall that Babel strips types when Metro bundles and that tsc, not Metro, reports type errors.
Explain what @react-native/babel-preset does to .ts and .tsx files, and why the base tsconfig sets noEmit and isolatedModules.
Make type checking enforceable: tsc in CI and hooks, import type discipline, and agreement between Babel and tsc resolution.
Decide how strictly types gate merges and releases, balancing strictness settings against the speed of a Babel-only dev loop.
## Two tools, two jobs In a TypeScript React Native app, the TypeScript source goes through **two separate tools** that never talk to each other: | Job | Tool | Configured by | Runs when | |---|---|---|---| | Turn `.ts`/`.tsx` into JavaScript for the bundle | **Babel**, inside Metro | `babel.config.js` with `@react-native/babel-preset` | every time Metro bundles | | Report type errors | **`tsc`**, the TypeScript compiler | `tsconfig.json` extending `@react-native/typescript-config` | when you run it: editor, script, CI | The React Native docs state the recommendation directly: TypeScript sources are transformed by Babel during bundling, and the TypeScript compiler should be used **only for type checking**, which is the default for new apps. ## What Babel does to TypeScript **Metro** is React Native's bundler. For each source file it calls a Babel transformer, and the project's `babel.config.js` normally contains just `presets: ['module:@react-native/babel-preset']`. That preset adds `@babel/plugin-transform-typescript` for files ending in `.ts`, and again with TSX parsing for `.tsx`, with namespaces allowed. The plugin's job is **type erasure**: it deletes annotations, interfaces, type aliases and `import type` statements, and leaves the JavaScript behind. It works on **one file at a time** and does **no type checking**. A wrong prop type, a missing null check or a misspelled property on a typed object all compile to perfectly valid JavaScript. What does stop the bundle is a **syntax** error: Babel cannot parse the file, so Metro reports it in the terminal and the app shows an error screen. ## What tsc does The base config `@react-native/typescript-config` sets, among other things: - `noEmit: true`: `tsc` writes no JavaScript, so it cannot conflict with Babel's output; - `strict: true`: the full set of strict checks; - `jsx: "react-native"`: JSX is left as JSX, which fits a type-checking-only setup; - `isolatedModules: true`: `tsc` reports constructs that a per-file transpiler like Babel cannot compile correctly, such as re-exporting a type without `export type`. Running `npx tsc` in the project root therefore means "check the whole program and report errors", nothing more. ## Consequences in a real team 1. **The app runs with type errors.** A developer who only watches the simulator will never see them. Editors show them inline, but only for open files. 2. **CI must run `tsc`.** A pipeline that only runs Jest and builds the app lets type errors reach `main`; Jest also compiles TypeScript through Babel. 3. **Type-only imports must be marked.** With `isolatedModules`, `import type` and `export type` make it unambiguous to Babel what to erase. 4. **Both configs must agree on resolution.** Babel and `tsc` resolve imports independently, which is why features like path aliases have to be configured in both. ## Common misconceptions - "Metro uses `tsc` to compile TypeScript." It does not; the TypeScript compiler is not part of the bundle pipeline. - "A green Metro build means the types are correct." It means the files parsed. - "`noEmit` disables type checking." It only disables output; the checks all still run. ## Where the entry file fits The docs add one practical rule when converting an existing app: rename components to `.tsx`, but **leave `index.js` as it is**, because renaming the entry point can cause trouble when bundling a production build. Files with a `.jsx` extension are treated as JavaScript and are not type-checked. ## Making type errors impossible to merge Because nothing in the React Native build fails on a type error, enforcement has to be added on purpose: - a `typecheck` script in `package.json` that runs `tsc`, so everyone runs the same command; - the editor's TypeScript service for immediate feedback while typing; - a pre-commit hook for fast local feedback, kept optional so it never blocks urgent fixes; - a **required CI check** that runs the script on every pull request, which is the step that actually guarantees clean types on the main branch; - an agreed policy for `// @ts-expect-error`, which documents an exception and fails once the error disappears, rather than `// @ts-ignore`, which hides it silently. With that in place, the fast Babel-only dev loop and the strict type checker complement each other: developers iterate without waiting for `tsc`, and the pipeline still refuses code that does not type-check.
- Why does @react-native/typescript-config enable isolatedModules?Babel compiles each file on its own, without the type information of other files. `isolatedModules` makes `tsc` report the constructs that cannot be compiled correctly that way, such as re-exporting a type without `export type`, so code that passes `tsc` also transpiles correctly in Metro.
- In a React Native repo, where should tsc run so type errors cannot be merged?In CI, as a required check on every pull request, typically as `npx tsc`; optionally also in a pre-commit hook for fast feedback. Metro and Jest both use Babel and will not fail on type errors, so without an explicit `tsc` step nothing enforces them.
- Does a syntax error behave differently from a type error in a React Native app?Yes. A syntax error stops Babel from parsing the file, so Metro fails the bundle and the app shows an error. A type error is invisible to Babel: the annotations are erased and the JavaScript runs, possibly failing later at runtime.
saying these in an interview costs you the question
- Metro runs the TypeScript compiler to build the bundle
- A successful Metro bundle proves the code type-checks
- noEmit: true turns off type checking
- Type errors show up as a red error screen in the app
- Jest tests will catch type errors because the tests are .tsx