DEV Community

Muhammad Mozzammel Haque
Muhammad Mozzammel Haque

Posted on

Metro doesn't type-check your React Native app, so I made Gradle do it

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

--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'
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
davekurian profile image
Dave Kurian •

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?