Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

In an effort to try to get TypeScript definitions for React Native consistent with the cononical Flow types, I have been looking into the possibility of automating this process.

My initial approach was to see how far I could get without any help needed from Facebook’s side. This process looks something like this:

  1. Transform Flow codebase to TypeScript by leveraging existing tooling with a few additional changes, which mostly replaces Flow utility types with TypeScript ones. (This tooling is presumably meant for people migrating from Flow to TypeScript and thus it is ok for these tools to not deliver perfect conversions.)
  2. Replace $ObjMap usage with a mapped type, as RN’s usage of $ObjMap is trivial (return types are static).
  3. Replace import type {Foo} from "./some-module" with import {Foo} from "./some-module".
  4. Replace module.exports = {…} with export default {…}.
  5. Replace const Foo = require("./some-module") calls with import Foo from "./some-module" statements and hoist+dedupe them.
  6. Replace import typeof Foo from "./some-module" with import _Foo from "./some-module"; type Foo = typeof _Foo;.
  7. Remove bodies from functions that have explicit return typing.
  8. Remove class members whose name starts with an underscore.
  9. Remove React.Component life-cycle related class members from subclasses and other implementation details such as its state typing.
  10. And finally feed this to tsc to compile, infer untyped return types, and emit declarations.

Much of this actually works surprisingly well, albeit slightly convoluted, but I did hit a few snags related to CommonJS imports/exports:

default export of single item and import typeof (fixable in TS type system)

For example, a CommonJS export like the following:

const Foo = {}
module.exports = Foo

…converted to:

const Foo = {}
default export Foo

👍 …will work fine with TypeScript as a value:

import Foo from "./some-module"
// Foo is a value
console.log(Foo)

👍 …and when converted from import typeof:

import _Foo from "./some-module"
// The type of the object shape
type Foo = typeof _Foo

Equally a single exported class as a value will work fine:

class Foo {}
module.exports = Foo

…converted to:

class Foo {}
default export Foo

👍 …will work fine with TypeScript as a value:

import Foo from "./some-module"

// Foo is a value
console.log(new Foo())

👎 …however, in the case where import typeof Foo from "./some-module" was converted, we end up with the type of the static class members. Luckily this is fixable in a static manner for both objects and classes, albeit in a roundabout way:

type ClassInstanceOrType<T> = T extends { new(...args: any): any } ? InstanceType<T> : T

import _Foo from "./some-module"
// Refers to the instance type of class Foo
type Foo = ClassInstanceOrType<typeof _Foo>
export of multiple items and importing specific items (not easily fixable)

For example, a CommonJS export like the following:

const Foo = {}

class Bar {}

module.exports = { Foo, Bar }

…when converted to a single default export:

const Foo = {}

class Bar {}

export default { Foo, Bar }

…means that TypeScripts users won’t be able to do:

import { Foo } from "./some-module"

…instead they would have to do:

import SomeModule from "./some-module"
const { Foo } = SomeModule

This is, in my opinion, too counterintuitive to be acceptable.

Ideally the exports would not use a single object, but multiple ES6 exports:

const Foo = {}

class Bar {}

export { Foo, Bar }

…but then RN code that does try to capture all exports in a single object would have to be re-written like the following, which makes this process harder to do as a static conversion (that has no context about other modules):

import * as SomeModule from "./some-module"
const { Foo } = SomeModule

This becomes even more problematic to convert when the export object has lazy getters:

module.exports = {
    get Foo() {
        return require("./foo")
    }
}

I’m unsure yet how to proceed; mostly I think I need to sleep on it for a bit while I switch gears and get this out of my head in front of you all. Options that currently are running through my head are:

  • Make RN use ES6 imports/exports so these don’t need to be converted; but I’m sure there are reasons why this isn’t done yet.
  • Ask the Flow team to make the CLI tool able to emit Flow declarations that use ES6 imports/exports.

The latter might be preferable anyways, as it would remove the need for tsc to infer untyped return types–which in turn means we can do a straight conversion and not have tsc introduce inline types or other such behaviour that’s hard to control.

To be continued…

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.