React-native-web: Exporting types

Created on 24 Jul 2020  路  28Comments  路  Source: necolas/react-native-web

I'm looking for input from people who use typed JavaScript with this library.

  1. If you use Flow or TypeScript, how do you type react-native-web exports?
  2. If you work across platforms, how do you deal with platform-specific differences in the APIs or components like View?
  3. Does this library need to export Flow and TypeScript types?
  4. Is there a way to generate TypeScript interfaces from Flow types so you don't need to maintain both?
help

Most helpful comment

Some good news: @types/react-native seems to have implemented the fix suggested here, which means declaration merges works for TypeScript in the case of Pressable.

You can now do this in your app:

// react-native-web/overrides.ts

declare module 'react-native' {
  interface PressableStateCallbackType {
    hovered?: boolean
    focused?: boolean
  }
}

And you'll get type support for Pressable, for instance.

I think it would make sense to have one TS file in react-native-web that includes all of the declaration merging, such as the code sample above. This way, anyone who uses react-native-web gets upgraded types. For those who do not, nothing changes.

All 28 comments

Here is a useful dialog about TypeScript in RNW: https://github.com/necolas/react-native-web/issues/832

There is no DefinitelyTyped package for react-native-web. I'm not entirely sure how one would work (efficiently). I've seen people propose copying all react-native types and then extending them, and somehow aliasing types/react-native to types/react-native-web. This of course wouldn't work with other out-of-tree solutions so I would discourage the approach.

Personally I would opt for universal packages like "react animated" or "react text" (probably not with these names since they're all taken). These universal packages would implement iOS, Android, and web in the same place. Said approach would also fix some versioning issues and help reduce the custom bundler configuration required for universal React apps.

I will note that RN is now moving towards a monorepo structure (creating packages like react-native/normalize-color color and react-native/polyfills) which could make modifications like this more feasible.

At Expo, we've refactored all of our universal React packages from flow to TypeScript (as opposed to adding external type definitions files .d.ts) because we find this is easier to work with from a DX perspective.

I personally think that having better types will help bring more visibility to important react-native-web features (e.g. href on Text). Due to the high demand in the Expo/React community for TypeScript support on web, I may look at creating some abstract packages which reexport react-native(-web) modules with proper types.

In Build Tracker (typescript, web-only), I pulled the react-native types from definitely-typed, then removed all of the iOS/Android specific stuff and added web-specific bits (source). Ideally I would take the few minutes to contribute this back, but I don't feel comfortable enough with my knowledge on Typescript to really be authoritative that any of it was done correctly.

At Twitter, we have our own flow definitions for react-native-web, but had built this up before this package was exporting most of its types. Ideally, we would love to use the source here, but have found that our type definitions tend to be more strict (which we like).

Is there a way to generate TypeScript interfaces from Flow types so you don't need to maintain both?

There is an inverse for this using flow-typed, though it's not 100% working. I've found it work for maybe 75% of the times I've used it. The times it didn't, some light hand-editing fixed it.

@EvanBacon I don't know that much of what you suggested is actionable. I'm a bit concerned about the implication that you're going to repackage everything under different packages

@paularmstrong I wasn't aware this package was exporting types at all 馃槄 Would be happy to hear about what your flow types look like and what stricter types could be upstreamed

Ah, they're not exporting types, correct. I believe I had done something with the module.name_mapper to re-map to the src files. It worked out okay, but not everything was fully typed at the time. And the main index.js not having // @flow at the top was problematic as well.

I'm not as connected with that area of our code anymore, so I'm not certain what we have that's more strict. Maybe @comp615 has some thoughts on that.

The one thing that I've noticed that's really nice about the TypeScript types from react-native that I'm using in Build Tracker is that the StyleSheet props are fully typed and throws errors if you use a style that isn't allowed (like color on a View doesn't work, but will on Text). We definitely don't have that same level of strict typing in Flow.

I filed a ticket during the last update (pre 0.13) to look into using the RNW types by default (with Paul's mapper hack) instead of our typedef module file. As @paularmstrong mentioned, they are not currently exported in RNW, which would be a great (and I think easy?) starting point to alleviate some of the boilerplate.

In particular, it would really help with major updates since it makes it much more trivial to track down all the things that changed and have certainty around them since they'd rely on the real source definition and not our manual list. I found it very useful last upgrade to read the release notes and update our flow file accordingly, and then go fix each usage in our code where there was a flow error.

What's the best way to export the types? Babel is currently stripping them when it compiles the files.

flow-copy-source or DIY copy every file over to your dist directory and add .flow as the final extension. Probably also need to ensure // @flow is at the top of every file that supports types

Environment:

  • expo SDK v38 (managed workflow)
  • "@types/react-native": "~0.63.6"
  • "react-native-web": "~0.13.5"
  • "typescript": "~3.9.7"
  • "react": "0.0.0-experimental-4c8c98ab9"
  • "react-dom": "0.0.0-experimental-4c8c98ab9"

I do not use any new API from RN v0.63, so so far no problem.

For my project, for any cross-platform components,

two cases:

  1. the native and web share the same signature.

    • I create the components.tsx and components.web.tsx, and always use components.tsx as the source when importing.

  2. the native and web share a different signature.

    • I create a single entry like component.tsx and deal with the platform difference from there, like the above and platform check.

And all the type info, I just use from @types/react-native

No problems and works great.

So, I always share the same signature across platforms. Because I do not want to propagate the platform differences to the caller, so the vast majority of the codebase can be platform agnostic.

In short,

I think if react-native-web can maintain an exact same type info match to @types/react-native, that will be awesome, but if there are any differences, just need to doc it, and I am fine with that, that just means, for any differences, we might need to create this one more abstraction (for example different file extension and implementation for a different platform) for every difference that we are using in the user-land code, but that is totally understandable.

But if the API surface can be 100% percent compatible (for non-supported, won't throw an error), then it should be fine to just use the @types/react-native, otherwise, seems unnecessary and maintaining the types are a big task

thanks for the awesome package, so beautiful.

I came here because I was shocked to find out that there's no Typescript types

Well, RN is Flow typed and doesn't provide TS types, so I don't know what you're pretending to be so shocked about.

I would advice leveraging on @types/react-native instead

Those types aren't accurate for Windows or Web.

Well, RN is Flow typed and doesn't provide TS types, so I don't know what you're pretending to be so shocked about.

Most RN libraries provide types.

Those types aren't accurate for Windows or Web.

Can you give examples of which parts aren't accurate?

Offhand I can only think of ScrollView: { bounces: boolean }, which itself isn't even accurate for mobile because it's not supported on Android.

I'm not entirely sure how one would work (efficiently). I've seen people propose copying all react-native types and then extending them, and somehow aliasing types/react-native to types/react-native-web. This of course wouldn't work with other out-of-tree solutions so I would discourage the approach.

@EvanBacon, At You.i TV we have a out of tree RN solution to extend to 11+ platforms and I did just that for our type definitions. I extend from the official RN types and Omit/Pick the types as needed. You mention, "This of course wouldn't work with other out-of-tree solutions so I would discourage the approach." I fail to see how this is the case. Can you please explain?

As omitting/picking the types is a job of its own, for now I am happy to have all RN types for RN Web vs none. So I just pointed the RN types to it with: yarn add -D @types/react-native-web@npm:@types/react-native

You can also just install @types/react-native and point the type package to react-native-web through your tsconfig.js:

{
  "compilerOptions": {
    ...
    "baseUrl": "./", // Required with the use of paths. May differ depending on project structure.
    "paths": {
      "@types/react-native-web": ["./node_modules/@types/react-native"]
    },
    ...
  }
}

@necolas, I could look into writing the initial TypeScript definition file for this library. I see that the Flow types are include in the repo. I am not experienced with Flow, however would you like to the types to be included here (like Flow) or in Definitely Typed?

Could TypeScript declaration merging solve this?

That's what I'm using in my current project.

Example

// react-native-web/overrides.ts

declare module 'react-native' {
  interface PressableStateCallbackType {
    hovered: boolean
  }
}

That would extend the existing PressableStateCallbackType from @types/react-native (with one caveat, more on that below.)

Screen Shot 2020-10-19 at 12 32 15 PM

Caveat

There is one problem with this approach at the moment. @types/react-native exports certain types as type rather than interface. This prevents declaration merging.

For instance, this is the current type for Pressable's callback:

// @types/react-native/index.d.ts line 473
export type PressableStateCallbackType = Readonly<{
    pressed: boolean;
}>;

Changing it to this makes it extensible:

export interface PressableStateCallbackType {
    pressed: boolean;
}

Thus, declaration merging could (in a few cases) require a PR to @types/react-native turning type into interface.

I think ReactNative could have web type too. ReactNative supported specific type for android or iOS that could support specific type for web too.

I don't think React Native wants that as the react-native-macos, react-native-web, react-native-windows are not 'official'. Maybe it would be better to create a different type system which has special properties for all these platforms available, but that would require a lot of work.

Maybe we will have to wait for React Native to add iOS Pointer support as this would probably create the hovered in React Native itself for iOS:
https://react-native.canny.io/feature-requests/p/ios-pointer-support

I do not agree with you because React Native added web to official documentation but not added react-native-macos or react-native-windows

If you use Flow or TypeScript, how do you type react-native-web exports?
I use @types/react-native. When there are missing types for props that are supported on react-native-web I wrap the component with right types for props, and pass though to react-native component. But I like @nandorojo approach as well.

If you work across platforms, how do you deal with platform-specific differences in the APIs or components like View?
Same as above, but I will add comments to the prop type.

Does this library need to export Flow and TypeScript types?
I don't think so. I would rely on @types/react-native only, and patch the differences with "wrapper" components

Is there a way to generate TypeScript interfaces from Flow types so you don't need to maintain both?
I don't know.

In my app, I use patch-package to change @types/react-native's read-only types from type to interface. This change addresses the caveat I mentioned above.

I then use declaration merging in my own files.

Until there is a better solution, I'll be sticking with that. I can put together a gist if it would be useful.

Re: the questions:

  1. TypeScript declaration merging
  2. Yes, it should do TypeScript declaration merging for you. I don't know about Flow.

I'm not a big fan of creating my own View just for type support, so I prefer that approach.

In my app, I use patch-package to change @types/react-native's read-only types from type to interface. This change addresses the caveat I mentioned above.

I then use declaration merging in my own files.

Until there is a better solution, I'll be sticking with that. I can put together a gist if it would be useful.

Re: the questions:

  1. TypeScript declaration merging
  2. Yes, it should do TypeScript declaration merging for you. I don't know about Flow.

I'm not a big fan of creating my own View just for type support, so I prefer that approach.

What about when you need to overwrite an existing field? For example if I need to add "fixed" in position type:

declare module "react-native" {
  interface FlexStyle {
    position?: "absolute" | "relative" | "fixed";
  }
}

It complains about position being already declared.

I've tried to avoid fixed position and things that RN doesn't support, but when you can't, you might need to use patch-package to edit the react native types package.

Similarly, borderRadius, translateY, translateX, and fontSize only allows number in RN, but in RNW it can also be set to a string. RNW also allows me to use the AccessibilityRole's list and listitem, not present in RN's type.

I actually do this naive transformation in a node script on postinstall:

const RN_TSD = process.cwd() + '/node_modules/@types/react-native/index.d.ts';
const raw = fs.readFileSync(RN_TSD);
let transformed = raw.toString();

transformed = transformed.replace('borderRadius?: number;', 'borderRadius?: number | string;');
/ * ... */

fs.writeFileSync(RN_TSD, transformed);

I know it will break on a change in the type, but it satisfies me for now.

@baptisteArno Should work if you change FlexStyle to ViewStyle

Some good news: @types/react-native seems to have implemented the fix suggested here, which means declaration merges works for TypeScript in the case of Pressable.

You can now do this in your app:

// react-native-web/overrides.ts

declare module 'react-native' {
  interface PressableStateCallbackType {
    hovered?: boolean
    focused?: boolean
  }
}

And you'll get type support for Pressable, for instance.

I think it would make sense to have one TS file in react-native-web that includes all of the declaration merging, such as the code sample above. This way, anyone who uses react-native-web gets upgraded types. For those who do not, nothing changes.

Sorry to keep adding messages here, but I think the solution is to add a declaration-merging.ts file /types. Similar to the flow files that are already there, it will look like this:

declare module 'react-native' {
  interface PressableStateCallbackType {
    hovered?: boolean
    focused?: boolean
  }
  interface ViewStyle {
    transitionProperty?: string
    transitionDuration?: string
  } 
  // ...etc
}

I only added a few types there, but it would be easy to add the rest of the RNW-specific props. This would solve the problem for TypeScript users.

You should install react-native types

   npm install @types/react-native

And redirect them to react-native-web types in tsconfig.json

    "paths": {
      "@types/react-native-web": ["../node_modules/@types/react-native"],
      "react-native": ["../node_modules/react-native-modules"]
    }

I played around most of today with Flow and trying to get the types to just pop up using flow-copy-source. I was able to get flow updated to the latest version, including re-copying some of the vendored code from RN, and doing some code mods. See that all here: https://github.com/comp615/react-native-web/tree/flow.

However, when I tried to use this in a sample expo projects. I still got a fair number of flow errors from:

  • modules (i.e. normalize-css-color) because presumably when flow goes to look at the .js.flow file, those imports are still there and it still tries to resolve them.
  • src / test files. These shouldn't really be scanned by the test project. I played around with various flowconfigs to ignore them, but that seems like something the project should have to do. Potentially caused by using yarn link instead of a normal build.

The code to update flow from that branch is 99% good, with the package change to copy sources being the bit I'd pull out.

I'd like to try it again with a create-react-app or Twitter to see if a different project setup works more easily. Expo seems to just work with react-native because RN is directly linked from source and not compiled through babel.

As a side musing...flow internally via types-first seems to keep some externally visible signature for each file. It would be awesome if it would just dump those as .flow.js files so we didn't have to copy source as lib authors.

@necolas is it solved typescript issue?

The current commit doesn't fix it for TypeScript users.

I mentioned the TS solution in comment above.

react-native-tvos uses the strategy I mentioned. You can see their types declaration file here.

Maybe there's a way to turn the generated flow files into TS types.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

shirakaba picture shirakaba  路  3Comments

tgh picture tgh  路  3Comments

roryabraham picture roryabraham  路  3Comments

zhangking picture zhangking  路  3Comments

MovingGifts picture MovingGifts  路  3Comments