This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.
教程:生成模块 TS 接口
关于使用 expo-type-information 包为 Expo 模块创建 TypeScript 接口的教程。
警告 本教程适用于 macOS 用户,因为
expo-type-information包仅在 macOS 上可用。
编写 Expo 模块通常意味着需要多次编写模块接口:在 Swift、Kotlin 和 TypeScript 中。expo-type-information 包通过直接从你的 Swift 代码中提取类型定义来生成 TypeScript 接口,从而实现自动化。
🌐 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.
在本教程中,你将学习如何使用 expo-type-information 包为 内联模块 和 常规 Expo 模块 生成 TypeScript 接口。
🌐 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
首先安装 expo-type-information 软件包。
🌐 First install the expo-type-information package.
- npm install expo-type-information要使用此软件包,你还需要安装 sourcekitten。
🌐 To use this package you also need to have sourcekitten installed.
- 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:
appFirstInlineModule.ktFirstInlineModule.swiftFirstInlineView.swiftFirstInlineView.ktindex.ts请记住,我们以前有一个索引文件,在其中我们直接使用 requireNativeModule 和 requireNativeView 引用内联模块,然而它们返回的是 any 类型,这种类型不提供类型安全,也不允许自动补齐。
🌐 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.
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://expo.nodejs.cn/modules/" /> </> ); } const styles = StyleSheet.create({ textBox: { height: 100, justifyContent: 'flex-end', alignItems: 'center' }, text: { fontSize: 26 }, inlineView: { flex: 1 }, });
我们现在将使用 expo-type-information 包为这些模块生成 TS 接口。在项目根目录下运行以下命令:
🌐 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 --watcher--app-json(简称 -a)指定应用配置文件的路径,其中定义了内联模块的 watchedDirectories。当使用 --watcher(简称 -w)选项时,应用配置文件和所有 watchedDirectories 都会被监视,并且这些更改时 TS 接口将会重新生成。
🌐 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.
运行此命令后,你应该会在项目中看到 4 个新文件:
🌐 After running this command you should see 4 new files in your project:
appFirstInlineModule.generated.tsFirstInlineModule.tsxFirstInlineView.generated.tsFirstInlineView.tsx让我们先来看一下 FirstInlineModule.swift。对于你项目中的每个内联模块,将创建两个文件,在本例中是 FirstInlineModule.generated.ts 和 FirstInlineModule.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.
/*Automatically generated by expo-type-information.*/ import { ViewProps } from 'react-native'; import { NativeModule } from 'expo'; export declare class FirstInlineModuleNativeModuleType extends NativeModule { readonly Hello: string; }
这个“生成”的文件包含了每种已解析的类型,在我们的例子中,它只包含了 FirstInlineModule 的定义,FirstInlineModule 中只声明了一个类型为 string 的 Hello 常量。再次运行该命令或使用 --watcher 运行时,该文件将被重新生成,因此不要修改它,除非你不再想使用该命令。
🌐 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.
让我们看看为 FirstInlineModule 生成的另一个文件
🌐 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;
此文件应当是你模块的“稳定”接口。CLI 使用文件哈希来检测手动更改。如果你自定义此文件,CLI 将停止覆盖它,从而允许你添加自定义逻辑或辅助函数,同时保持“生成”文件中的原生类型同步。
🌐 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.
在我们的案例中,我们只是从原生模块重新导出 Hello 常量。
🌐 In our case we just reexport the Hello constant from the native module.
现在让我们来看看为 FirstInlineView.swift 生成的文件
🌐 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 {}
查看“generated”文件,我们可以看到并非所有来自 FirstInlineView.swift 的类型都已被解析,某个 URL 类型被设置为 unknown。当类型不是基本类型(这些类型由工具手动映射)且未在提供的文件中定义时(在这种情况下,它未在 FirstInlineView.swift 中定义),或者工具未能解析其定义时,可能会发生这种情况。
🌐 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.
我们还可以看到一个 ExpoWebViewProps 接口已经生成,它具有来自 FirstInlineView 的属性和事件。
🌐 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} />; }
“stable” 文件与之前的情况也有些不同。它现在有一个默认导出,其中 ExpoWebViewComponent 封装了原生的 FirstInlineView 视图。不过请注意,由于这是一个默认导出,在内联模块中只能定义一个视图,才能使这个 “stable” 文件正常工作。
🌐 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.
有了这些生成的文件,我们现在可以轻松地在 TypeScript 中使用内联模块和内联视图。app/index.tsx 曾经是
🌐 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://expo.nodejs.cn/modules/" /> </> ); } const styles = StyleSheet.create({ textBox: { height: 100, justifyContent: 'flex-end', alignItems: 'center' }, text: { fontSize: 26 }, inlineView: { flex: 1 }, });
我们现在可以删除 requireNativeModule 并从“稳定”文件中导入模块和视图。
🌐 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://expo.nodejs.cn/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 --watcher让我们给 FirstInlineModule 添加一个新函数:
🌐 And 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 } } } }
在将新的 ConcatStrings 函数添加到 Swift 模块文件后,你应该会看到“generated”和“stable”文件已被更新,并且现在也包含 ConcatStrings 函数。
🌐 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); }
如果此文件尚未更新,你很可能已经更改了它!如果你想重新生成它,你需要先删除它,然后修改 Swift 模块以触发监视器。
🌐 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://expo.nodejs.cn/modules/" /> </> ); } const styles = StyleSheet.create({ textBox: { height: 100, justifyContent: 'flex-end', alignItems: 'center' }, text: { fontSize: 26 }, inlineView: { flex: 1 }, });
这就结束了关于如何生成和使用内联模块 TypeScript 接口的教程。
🌐 That concludes the tutorial on how to generate and use the TypeScript interface for inline modules.
Expo 模块接口
🌐 Expo module interface
让我们基于 native-module-tutorial 示例 来构建。在该示例中,你创建了一个 expo-settings 模块,其中定义了一个简单的 Swift 模块。
🌐 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 } }
你还为此模块创建了一个 TypeScript 接口,该模块由以下文件组成:
🌐 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
现在让我们使用 expo-type-information 命令行工具来自动生成这个接口,而不是手动编写它!
🌐 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-settings如果你打算更改文件并想查看界面如何变化,请在命令中添加 --watcher(简写 -w)标志。
🌐 If 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 -w--module 选项(简写 -m)是模块根文件夹的路径。运行此命令后,你应该能在模块包中看到 3 个新生成的文件。
🌐 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.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, }
*.types.ts 文件包含模块中定义的所有类型的定义。在我们的案例中,我们只声明了一个 Theme 枚举,该枚举已经被正确地放入文件中。但是请注意,与教程中的 ExpoSettings.types.ts 相比,事件类型尚未生成。expo-type-information 工具是新的且功能强大,但并非每个选项都已实现,模块事件就是其中之一。
🌐 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;
*Module.ts 文件包含了本地模块类的声明,并且它导出了模块实例。请注意,类似于前一个文件,我们在这里没有定义事件。
🌐 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.