Tutorial: Generate module TS interface
Edit page
A tutorial on using the expo-type-information package to create TypeScript interface for an Expo module.
For the complete documentation index, see llms.txt. Use this file to discover all available pages.
This tutorial is for macOS users, as theexpo-type-informationpackage 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.
- npm install expo-type-informationTo use this package you also need to have sourcekitten installed.
- brew install sourcekittenGenerating 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:
appFirstInlineModule.ktFirstInlineModule.swiftFirstInlineView.swiftFirstInlineView.ktindex.tsRemember 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.
import { requireNativeModule, requireNativeView } from 'expo'; import { StyleSheet, Text, View } from 'react-native'; const FirstInlineModule = requireNativeModule('FirstInlineModule'); const FirstInlineView = requireNativeView('FirstInlineView'); export default function InlineModulesDemoComponent() { return ( <> <View style={styles.textBox}> <Text style={styles.text}> {FirstInlineModule.Hello} </Text> </View> <FirstInlineView style={styles.inlineView} url="https://docs.expo.dev/modules/" /> </> ); } const styles = StyleSheet.create({ textBox: { height: 100, justifyContent: 'flex-end', alignItems: 'center' }, text: { fontSize: 26 }, inlineView: { flex: 1 }, });
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:
- npx expo-type-information inline-modules-interface --app-json ./app.json --watcherThe --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:
appFirstInlineModule.generated.tsFirstInlineModule.tsxFirstInlineView.generated.tsFirstInlineView.tsxLet'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.
/*Automatically generated by expo-type-information.*/ import { ViewProps } from 'react-native'; import { NativeModule } from 'expo'; export declare class FirstInlineModuleNativeModuleType extends NativeModule { readonly Hello: 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
// File hash: c7729100cc23e11d5d39fcb99fe861f7b03502986ee7becb85731cb631f37000 import { FirstInlineModuleNativeModuleType } from './FirstInlineModule.generated'; import { requireNativeModule, requireNativeView } from 'expo'; const FirstInlineModule: FirstInlineModuleNativeModuleType = requireNativeModule<FirstInlineModuleNativeModuleType>('FirstInlineModule'); export const Hello: string = FirstInlineModule.Hello;
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
/*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). export type URL = unknown; export interface ExpoWebViewProps extends ViewProps { url: URL; onLoad?: (event: any) => void; } export declare class FirstInlineViewNativeModuleType extends NativeModule {}
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.
// File hash: 6eb6c583bee1f61cbb9f6557faadc9d6b7fb51313c05027076431304668f7ac5 import React from 'react'; import { URL, FirstInlineViewNativeModuleType, ExpoWebViewProps, } from './FirstInlineView.generated'; import { requireNativeModule, requireNativeView } from 'expo'; const FirstInlineView: FirstInlineViewNativeModuleType = requireNativeModule<FirstInlineViewNativeModuleType>('FirstInlineView'); const ExpoWebView = requireNativeView<ExpoWebViewProps>('FirstInlineView', 'ExpoWebView'); export default function ExpoWebViewComponent(props: ExpoWebViewProps) { return <ExpoWebView {...props} />; }
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
import { requireNativeModule, requireNativeView } from 'expo'; import { StyleSheet, Text, View } from 'react-native'; import * as React from 'react'; const FirstInlineModule = requireNativeModule('FirstInlineModule'); const FirstInlineView = requireNativeView('FirstInlineView'); export default function InlineModulesDemoComponent() { return ( <> <View style={styles.textBox}> <Text style={styles.text}> {FirstInlineModule.Hello} </Text> </View> <FirstInlineView style={styles.inlineView} url="https://docs.expo.dev/modules/" /> </> ); } const styles = StyleSheet.create({ textBox: { height: 100, justifyContent: 'flex-end', alignItems: 'center' }, text: { fontSize: 26 }, inlineView: { flex: 1 }, });
We can now drop the requireNativeModule and import the module and view from the "stable" files.
import { StyleSheet, Text, View } from 'react-native'; import * as React from 'react'; import { Hello } from './FirstInlineModule'; import FirstInlineView from './FirstInlineView'; export default function InlineModulesDemoComponent() { return ( <> <View style={styles.textBox}> <Text style={styles.text}> {Hello} </Text> </View> <FirstInlineView style={styles.inlineView} url="https://docs.expo.dev/modules/" /> </> ); } const styles = StyleSheet.create({ textBox: { height: 100, justifyContent: 'flex-end', alignItems: 'center' }, text: { fontSize: 26 }, inlineView: { flex: 1 }, });
Watcher
To see the watcher in action make sure that you have the previous command still running
- npx expo-type-information inline-modules-interface --app-json ./app.json --watcherAnd let's add a new function to the FirstInlineModule:
internal import ExpoModulesCore class FirstInlineModule: Module { public func definition() -> ModuleDefinition { Constant("Hello") { return "Hello iOS inline modules!" } Function("ConcatStrings") { (str1: String, strings: [String]) -> String in return strings.reduce(str1) { $0 + $1 } } } }
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.
/*Automatically generated by expo-type-information.*/ import { ViewProps } from 'react-native'; import { NativeModule } from 'expo'; export declare class FirstInlineModuleNativeModuleType extends NativeModule { readonly Hello: string; ConcatStrings(str1: string, strings: string[]): string; }
// File hash: 2311de57c3a0c2135c45f49be6d2fdccd57a2b65c0ad10285c248bdf5276b7b6 import { FirstInlineModuleNativeModuleType } from './FirstInlineModule.generated'; import { requireNativeModule, requireNativeView } from 'expo'; const FirstInlineModule: FirstInlineModuleNativeModuleType = requireNativeModule<FirstInlineModuleNativeModuleType>('FirstInlineModule'); export const Hello: string = FirstInlineModule.Hello; export function ConcatStrings(str1: string, strings: string[]) { return FirstInlineModule.ConcatStrings(str1, strings); }
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.
You can now use the new function in your app
import { StyleSheet, Text, View } from 'react-native'; import * as React from 'react'; import { Hello, ConcatStrings } from './FirstInlineModule'; import FirstInlineView from './FirstInlineView'; export default function InlineModulesDemoComponent() { return ( <> <View style={styles.textBox}> <Text style={styles.text}> {Hello} </Text> <Text style={styles.text}> {ConcatStrings('Nicely ', ['typed ', 'function ', 'which ', 'concatenates ', 'strings!'])} </Text> </View> <FirstInlineView style={styles.inlineView} url="https://docs.expo.dev/modules/" /> </> ); } const styles = StyleSheet.create({ textBox: { height: 100, justifyContent: 'flex-end', alignItems: 'center' }, text: { fontSize: 26 }, inlineView: { flex: 1 }, });
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.
import ExpoModulesCore public class ExpoSettingsModule: Module { public func definition() -> ModuleDefinition { Name("ExpoSettings") Events("onChangeTheme") Function("setTheme") { (theme: Theme) -> Void in UserDefaults.standard.set(theme.rawValue, forKey:"theme") sendEvent("onChangeTheme", [ "theme": theme.rawValue ]) } Function("getTheme") { () -> String in UserDefaults.standard.string(forKey: "theme") ?? Theme.system.rawValue } } enum Theme: 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.tsexpo-settings/src/ExpoSettingsModule.tsexpo-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.
Now run a command to generate the interface
- npx expo-type-information module-interface --module ./expo-settingsIf you intend on changing the file and want to see how the interface changes, add the --watcher (short -w) flag to the command.
- npx expo-type-information module-interface --module ./expo-settings -wThe --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.tsexpo-settings/src/ExpoSettingsModule.tsexpo-settings/src/index.ts
Now let's look at what was generated.
// File hash: 455b035995710b95054ffc0fa6ee888d3be158c5145e64ce4b8e0a3a92c5c510 /*Automatically generated by expo-type-information.*/ import { ViewProps } from 'react-native'; import { NativeModule } from 'expo'; export enum Theme { 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.
// File hash: 21a1653e3cadc31ac359d32209987615e0feb20925c494864a9038013a3416b6 /*Automatically generated by expo-type-information.*/ import { requireNativeModule, NativeModule } from 'expo'; import { Theme } from './ExpoSettings.types'; export declare class ExpoSettings extends NativeModule { setTheme(theme: Theme): void; getTheme(): string; } const _default: ExpoSettings = requireNativeModule<ExpoSettings>('ExpoSettings'); export default _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.
/*Automatically generated by expo-type-information.*/ export type * from './ExpoSettings.types'; export { default as ExpoSettings } 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.