React Native never type-checks your TypeScript when it builds. Metro hands every file to Babel, Babel deletes the type annotations, and the bundle ships. A function that expects a number and gets a string bundles without a single warning. You find out at runtime, usually on a screen you didn't open while testing.
The editor underlines the error in red, so it's easy to assume the build checks it too. It doesn't. Only tsc does, and nothing runs tsc unless you tell it to.
I made type errors fail the build. It takes two pieces.
The npm script
In package.json:
{
"scripts": {
"typecheck": "tsc --noEmit",
"android": "npm run typecheck && react-native run-android"
}
}
--noEmit is the important flag. Metro already produces the JavaScript, so tsc only has to check.
The Gradle hook
The script covers anyone who starts the app with npm run android. It does nothing when someone presses Run in Android Studio, or when CI calls ./gradlew assembleRelease directly. So I hooked the same check into Gradle, in android/app/build.gradle:
def isWindows = System.getProperty('os.name').toLowerCase().contains('windows')
tasks.register('typecheck', Exec) {
workingDir = rootProject.projectDir.parentFile
commandLine isWindows ? ['cmd', '/c', 'npx', 'tsc', '--noEmit'] : ['npx', 'tsc', '--noEmit']
}
preBuild.dependsOn 'typecheck'
The Windows branch exists because Gradle's Exec can't launch npx directly there. On Windows npx is a .cmd shim, so it has to go through cmd /c.
When tsc finds an error it exits non-zero, the Exec task fails, and Gradle stops before it compiles anything.
This puts a full type-check in front of every Android build, so on a large codebase you'll feel it. If that gets annoying during development, keep the npm script for local runs and attach the Gradle task only to release builds.
Top comments (1)
One thing to check on the Gradle hook: an Exec task with no declared inputs or outputs is never up to date, so it re-runs a full tsc on every build, including incremental debug builds. If you declare the sources as inputs and a marker file as output, Gradle skips it when nothing changed:
inputs.files(fileTree(dir: rootProject.projectDir.parentFile, includes: ['src/**', 'tsconfig.json', 'package.json']))
outputs.file(layout.buildDirectory.file('typecheck.stamp'))
doLast { file("$buildDir/typecheck.stamp").text = 'ok' }
That way you can leave preBuild.dependsOn on for debug as well and only pay the tsc cost after a TypeScript change. Did you measure how long the check adds on your largest app?