This tutorial is for macOS users, as the expo-type-information package works only on macOS.
Writing Expo Modules often means writing the module interface multiple times: in Swift, in Kotlin and in TypeScript. The expo-type-information package automates that by extracting type definitions directly from your Swift code to generate TypeScript interfaces.
In this tutorial you will learn how to use the expo-type-information package to generate TypeScript interface for both inline modules and regular Expo modules.
Setup your project
First install the expo-type-information package.
Terminal
- npm install expo-type-information
- yarn add expo-type-information
- pnpm add expo-type-information
- bun add expo-type-information
To use this package you also need to have sourcekitten installed.
Terminal
- brew install sourcekitten
Generating inline modules interface
Let's build on the inline modules example. In that tutorial we've built an example app with an inline module and an inline view. In your project you should have:
app
FirstInlineModule.kt
FirstInlineModule.swift
FirstInlineView.swift
FirstInlineView.kt
index.ts
Remember that we used to have an index file in which we directly reference inline modules using requireNativeModule and requireNativeView, however they return an any type which provides no type safety and doesn't allow for autocompletion.
We will now use the expo-type-information package to generate a TS interface for these modules. When in the root of the project run the following command:
The --app-json (short -a) specifies the path to the app configuration file, where watchedDirectories for inline modules are defined. When using --watcher (short -w) option the app configuration file and all watchedDirectories will be watched and TS interface will be regenerated when these change.
After running this command you should see 4 new files in your project:
app
FirstInlineModule.generated.ts
FirstInlineModule.tsx
FirstInlineView.generated.ts
FirstInlineView.tsx
Let's first look at the FirstInlineModule.swift. For each inline module in your project two files will be created, in this case FirstInlineModule.generated.ts and FirstInlineModule.tsx.
app/FirstInlineModule.generated.ts
/*Automatically generated by expo-type-information.*/import{ViewProps}from'react-native';import{NativeModule}from'expo';exportdeclareclassFirstInlineModuleNativeModuleTypeextendsNativeModule{readonlyHello:string;}
This "generated" file contains every type that was resolved, in our case it only has the definition of the FirstInlineModule, which only has a Hello constant of type string declared in it. When re-running the command or running it with --watcher this file will be regenerated, so do not change it, unless you don't want to use the command anymore.
Let's look at the other file generated for the FirstInlineModule
This file is supposed to be the "stable" interface for your module. The CLI uses the file hash to detect manual changes. If you customize this file, the CLI will stop overwriting it, allowing you to add custom logic or helper functions while keeping your native types synced in the "generated" file.
In our case we just reexport the Hello constant from the native module.
Now let's take a look at the files generated for FirstInlineView.swift
app/FirstInlineView.generated.ts
/*Automatically generated by expo-type-information.*/import{ViewProps}from'react-native';import{NativeModule}from'expo';// These types haven't been defined in provided file(s).exporttypeURL=unknown;exportinterfaceExpoWebViewPropsextendsViewProps{
url:URL;
onLoad?:(event:any)=>void;}exportdeclareclassFirstInlineViewNativeModuleTypeextendsNativeModule{}
Looking at the "generated" file, we can see that not all of the types from the FirstInlineView.swift could have been resolved, a URL type is set to unknown. This may happen when the type is not a basic type (which are mapped manually by the tool) and is not defined in provided files (in this case it hasn't been defined in the FirstInlineView.swift), or if the tool failed to parse its definition.
We can also see that a ExpoWebViewProps interface has been generated, it has the props and events from FirstInlineView.
The "stable" file is also a bit different than in the previous case. It now has a default export with a ExpoWebViewComponent wrapper over the native FirstInlineView view. Note however that as this is a default export there can only be one view defined in an inline-module, for this "stable" file to work properly.
With these generated files we can now easily use inline modules and inline views from TypeScript. The app/index.tsx used to be
After adding the new ConcatStrings function to the Swift module file you should see that the "generated" and "stable" file have been updated and now also contain the ConcatStrings function.
app/FirstInlineModule.generated.ts
/*Automatically generated by expo-type-information.*/import{ViewProps}from'react-native';import{NativeModule}from'expo';exportdeclareclassFirstInlineModuleNativeModuleTypeextendsNativeModule{readonlyHello:string;ConcatStrings(str1:string, strings:string[]):string;}
If this file hasn't been updated, you've probably changed it! If you want it regenerated, you need to remove it first and then change the Swift module to trigger the watcher.
That concludes the tutorial on how to generate and use the TypeScript interface for inline modules.
Expo module interface
Let's build on the native-module-tutorial example. In that example you've created an expo-settings module which had a simple Swift module defined inside it.
expo-settings/ios/SettingsModule.swift
importExpoModulesCorepublicclassExpoSettingsModule:Module{publicfuncdefinition()->ModuleDefinition{Name("ExpoSettings")Events("onChangeTheme")Function("setTheme"){(theme:Theme)->VoidinUserDefaults.standard.set(theme.rawValue, forKey:"theme")sendEvent("onChangeTheme",["theme": theme.rawValue
])}Function("getTheme"){()->StringinUserDefaults.standard.string(forKey:"theme")??Theme.system.rawValue
}}enumTheme:String,Enumerable{case light
case dark
case system
}}
You've also created a TypeScript interface for this module which consisted of files:
expo-settings/src/ExpoSettings.types.ts
expo-settings/src/ExpoSettingsModule.ts
expo-settings/src/index.ts
Now let's use expo-type-information CLI to generate this interface automatically, instead of writing it!
First remove the files above and make sure you're in the project root.
The --module option (short -m) is a path to the root folder of the module. After running this command you should see 3 new generated files in the module package.
expo-settings/src/ExpoSettings.types.ts
expo-settings/src/ExpoSettingsModule.ts
expo-settings/src/index.ts
Now let's look at what was generated.
expo-settings/src/ExpoSettings.types.ts
// File hash: 455b035995710b95054ffc0fa6ee888d3be158c5145e64ce4b8e0a3a92c5c510/*Automatically generated by expo-type-information.*/import{ViewProps}from'react-native';import{NativeModule}from'expo';exportenumTheme{
light,
dark,
system,}
The *.types.ts file contains the definitions of all types defined in the module. In our case we've only declared a Theme enum which has correctly been put in the file. Note however that in contrast to the ExpoSettings.types.ts from the tutorial, the events type have not been generated. The expo-type-information tool is new and powerful, however not every option is yet implemented, module events being one of them.
expo-settings/src/ExpoSettingsModule.ts
// File hash: 21a1653e3cadc31ac359d32209987615e0feb20925c494864a9038013a3416b6/*Automatically generated by expo-type-information.*/import{ requireNativeModule,NativeModule}from'expo';import{Theme}from'./ExpoSettings.types';exportdeclareclassExpoSettingsextendsNativeModule{setTheme(theme:Theme):void;getTheme():string;}const _default:ExpoSettings=requireNativeModule<ExpoSettings>('ExpoSettings');exportdefault _default;
The *Module.ts file contains the declaration of the native module class and it exports the module instance. Note that similar to the previous file, we don't have events defined in here.
expo-settings/src/index.ts
/*Automatically generated by expo-type-information.*/exporttype*from'./ExpoSettings.types';export{defaultasExpoSettings}from'./ExpoSettingsModule';
The index file differs from the example even more. Opposed to wrapping each module method in a separate function, we've opted to just reexport the module object.