This repo shows examples of of areas where react-native would benefit from a deeper integration with TypeScript. The problems areas are described below and expressed in code. The intent is to provide a playground for developing solutions to these problems.
The repo is currently "broken", meaning it can't build, bundle or provide full IntelliSense. Once the problems are fixed, everything should work.
Interesting commands (from the repo root):
yarn run build
yarn run bundle
There are two packages in this repo. Button
is a react-native-only control, and it is specialized per-platform. App
is a react-native application, which depends on Button
.
react-native tools use a specialized module resolver to enable platform-specific extensions. The tools all require a target platform, which they use when matching a module to a file. The resolver first looks for <module>.<platform>.js
, then <module>.native.js
, and finally <module>.js
.
The intermediate name native is used as a fallback. If found, the implementation
should be suitable for any react-native platform. This is useful for projects
that are a mix of react-native and web code. Web code lives in files without a platform
extension (<module>.js
).
TypeScript's resolver should be expanded to support a similar mechanism, and integrated with builds and IntelliSense. It should be generalized to be applicable outside of a react-native context.
You can see an example of this gap in the button index.ts file:
By default, react-native apps are built using native as the only fallback:
Platform | Fallback extensions (ordered) |
---|---|
ios | native |
android | native |
macos | native |
win32 | native |
windows | native |
App developers can change this, as needed. For example, this repo adds a 'win' fallback and a 'mobile' fallback:
Platform | Fallback extensions (ordered) |
---|---|
ios | mobile, native |
android | mobile, native |
macos | native |
win32 | win, native |
windows | win, native |
You can see these fallbacks defined in the Haul bundler configuration file haul.config.js.
When running a build or parsing for IntelliSense, TypeScript's resolver should be able to match a set of file-name patterns (the platform and its fallback(s)). This can be modeled in tsconfig:
{
"compilerOptions": {
// empty string --> match without an extension, e.g. /button.ts[x]?/
"moduleFileExtensions": ["ios", "mobile", "native", ""]
}
}
NOTE: The empty string is explicitly stated, giving developers control over the behavior. react-native apps will often co-exist alongside web apps, sharing non-UI logic and splitting out UI-specific code using the platform-extension mechanism. For these repos, react-native UI only lives in platform-extended files while web UI only lives in non-extended files.
Developers would have one tsconfig file per platform. The file name should not be constrained to any particular pattern. Many 3rd-party tools already support specifying a tsconfig file name, explicitly, which makes this approach a good choice.
IntelliSense will need additional support to make this work. With multiple tsconfigs in play, IntelliSense will need to be told which one to use, or it will default to loading tsconfig.json
. For react-native, I'm imagining this will be an IDE extension that exposes a platform selector in its config and/or in the UI. The corresponding tsconfig is passed to the IntelliSense tsserver
process.
Alternatively, the collection of all module file extension sets could be stored in a single tsconfig file:
{
"compilerOptions": {
"moduleFileExtensionProfiles": {
"ios": ["ios", "mobile", "native", ""],
"win32": ["win32", "win", "native", ""],
// ...
}
}
}
This is less desirable because the target set must be specified with each run of TypeScript. 3rd party packages that use TypeScript's API (e.g. webpack ts-loader plugin, api extractor) would need to expose a way to choose the target set.
And, similar to the Per-Platform TSConfig solution, IntelliSense will need to be told which target set to use.
React-native is implemented on many platforms which span several NPM packages. ios
and android
implementations are in the react-native
NPM package, which is maintained by Meta (Facebook). windows
is under react-native-windows
and macos
is under react-native-macos
, both of which are maintained by Microsoft. win32
is an Office-specific platform under @office-iss/react-native-win32
.
windows
, macos
, and win32
are all considered to be out-of-tree platforms because they aren't part of the core react-native
distribution.
Each platform package is a complete implementation of react-native, and has (or should have) associated TypeScript types.
To avoid having "forked" references to the various NPM package names in code, the react-native bundler maps imports of react-native
to the target out-of-tree platform package. For MacOS, import 'react-native'
becomes import 'react-native-macos'
.
TypeScript should support a similar mechanism, but only for type-checking and IntelliSense. Emitted code should preserve the original module import.
// Per-Platform TSConfig
{
"compilerOptions": {
"typeModuleMap": {
// this implies @types/react-native-macos, if needed
"react-native": "react-native-macos"
}
}
}
// Single TSConfig For All Platforms
{
"compilerOptions": {
"typeModuleMapProfiles": {
"macos": {
"react-native": "react-native-macos"
},
"windows": {
"react-native": "react-native-windows"
},
"win32": {
"react-native": "@office-iss/react-native-win32"
}
}
}
}
You can see an example of this in button.native.tsx. This file is used for both Android (in-tree platform) and MacOS (out-of-tree platform).