This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.
一篇关于使用 expo-type-information 包为 Expo 模块创建 TypeScript 接口的教程。
警告 本教程适用于 macOS 用户,因为 expo-type-information 包仅能在 macOS 上运行。
编写 Expo Modules 往往意味着需要多次编写模块接口:在 Swift 中、在 Kotlin 中以及在 TypeScript 中。expo-type-information 包通过直接从你的 Swift 代码中提取类型定义来生成 TypeScript 接口,从而自动化这一过程。
在本教程中,你将学习如何使用 expo-type-information 包为 inline modules 和 regular Expo modules 生成 TypeScript 接口。
设置你的项目
首先安装 expo-type-information 包。
- npm install expo-type-information
- yarn add expo-type-information
- pnpm add expo-type-information
- bun add expo-type-information
要使用此包,你还需要安装 sourcekitten。
- brew install sourcekitten
生成 inline modules 接口
我们将基于 inline modules 示例 继续。在该教程中,我们构建了一个包含 inline module 和 inline view 的示例应用。在你的项目中,你应该有:
请记住,我们曾经有一个 index 文件,在其中我们直接使用 requireNativeModule 和 requireNativeView 引用 inline modules,但它们返回的是 any 类型,既没有类型安全,也不支持自动补全。
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 } ,
} ) ;
现在我们将使用 expo-type-information 包为这些模块生成 TS 接口。在项目根目录运行以下命令:
- npx expo-type-information inline-modules-interface --app-json ./app.json --watcher
- yarn dlx expo-type-information inline-modules-interface --app-json ./app.json --watcher
- pnpm dlx expo-type-information inline-modules-interface --app-json ./app.json --watcher
- bunx expo-type-information inline-modules-interface --app-json ./app.json --watcher
--app-json(简写 -a)用于指定应用配置文件的路径,其中定义了 inline modules 的 watchedDirectories。使用 --watcher(简写 -w)选项时,应用配置文件以及所有 watchedDirectories 都会被监听,当它们发生变化时,TS 接口将被重新生成。
运行该命令后,你应该会在项目中看到 4 个新文件:
FirstInlineModule.generated.ts FirstInlineView.generated.ts
我们先看看 FirstInlineModule.swift。对于你项目中的每个 inline module,都会创建两个文件,在这个例子中是 FirstInlineModule.generated.ts 和 FirstInlineModule.tsx。
app/FirstInlineModule.generated.ts
import { ViewProps } from 'react-native' ;
import { NativeModule } from 'expo' ;
export declare class FirstInlineModuleNativeModuleType extends NativeModule {
readonly Hello : string ;
}
这个“generated”文件包含了所有已解析的类型,在我们的例子中,它只包含 FirstInlineModule 的定义,其中只声明了一个名为 Hello、类型为 string 的常量。再次运行该命令,或者在使用 --watcher 时,该文件都会被重新生成,所以不要修改它,除非你不再打算使用这个命令。
接下来看看为 FirstInlineModule 生成的另一个文件
app/FirstInlineModule.tsx
import { FirstInlineModuleNativeModuleType } from './FirstInlineModule.generated' ;
import { requireNativeModule, requireNativeView } from 'expo' ;
const FirstInlineModule : FirstInlineModuleNativeModuleType =
requireNativeModule < FirstInlineModuleNativeModuleType> ( 'FirstInlineModule' ) ;
export const Hello : string = FirstInlineModule . Hello ;
这个文件应该是你的模块的“稳定”接口。CLI 使用文件哈希来检测手动修改。如果你自定义了这个文件,CLI 将停止覆盖它,从而允许你添加自定义逻辑或辅助函数,同时让你的原生类型在“generated”文件中保持同步。
在我们的例子中,我们只是将原生模块中的 Hello 常量重新导出。
现在让我们看看为 FirstInlineView.swift 生成的文件
app/FirstInlineView.generated.ts
import { ViewProps } from 'react-native' ;
import { NativeModule } from 'expo' ;
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 中定义),或者工具未能解析其定义时也会出现。
我们还可以看到生成了一个 ExpoWebViewProps 接口,它包含了 FirstInlineView 的属性和事件。
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} /> ;
}
“稳定”文件在这里也与前一个例子略有不同。它现在有一个默认导出,包含一个包裹原生 FirstInlineView 视图的 ExpoWebViewComponent。不过请注意,由于这是一个默认导出,为了让这个“稳定”文件正常工作,inline-module 中只能定义一个视图。
有了这些生成文件后,我们现在就可以轻松地在 TypeScript 中使用 inline modules 和 inline views 了。app/index.tsx 之前是这样的
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 } ,
} ) ;
现在我们可以去掉 requireNativeModule,并从“稳定”文件中导入模块和视图。
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 } ,
} ) ;
监听器
要查看监听器的实际效果,请确保前面的命令仍在运行
- npx expo-type-information inline-modules-interface --app-json ./app.json --watcher
- yarn dlx expo-type-information inline-modules-interface --app-json ./app.json --watcher
- pnpm dlx expo-type-information inline-modules-interface --app-json ./app.json --watcher
- bunx expo-type-information inline-modules-interface --app-json ./app.json --watcher
然后我们给 FirstInlineModule 添加一个新函数:
app/FirstInlineModule.swift 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 }
}
}
}
在 Swift 模块文件中添加新的 ConcatStrings 函数后,你应该会看到“generated”和“稳定”文件已更新,并且现在也包含了 ConcatStrings 函数。
app/FirstInlineModule.generated.ts
import { ViewProps } from 'react-native' ;
import { NativeModule } from 'expo' ;
export declare class FirstInlineModuleNativeModuleType extends NativeModule {
readonly Hello : string ;
ConcatStrings ( str1: string , strings: string [ ] ) : string ;
}
app/FirstInlineModule.tsx
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 模块以触发监听器。
现在你可以在应用中使用这个新函数了
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 } ,
} ) ;
以上就是关于如何为 inline modules 生成并使用 TypeScript 接口的教程。
Expo 模块接口
让我们在 原生模块教程示例 的基础上继续。在那个示例中,你创建了一个 expo-settings 模块,其中定义了一个简单的 Swift 模块。
expo-settings/ios/SettingsModule.swift 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 接口,它由以下文件组成:
expo-settings/src/ExpoSettings.types.ts
expo-settings/src/ExpoSettingsModule.ts
expo-settings/src/index.ts
现在我们来使用 expo-type-information CLI 自动生成这个接口,而不是手动编写它!
首先删除上面的文件,并确保你位于项目根目录。
然后运行一条命令来生成接口
- npx expo-type-information module-interface --module ./expo-settings
- yarn dlx expo-type-information module-interface --module ./expo-settings
- pnpm dlx expo-type-information module-interface --module ./expo-settings
- bunx expo-type-information module-interface --module ./expo-settings
如果你打算修改文件,并想看看接口会如何变化,请在命令中添加 --watcher(简写 -w)标志。
- npx expo-type-information module-interface --module ./expo-settings -w
- yarn dlx expo-type-information module-interface --module ./expo-settings -w
- pnpm dlx expo-type-information module-interface --module ./expo-settings -w
- bunx expo-type-information module-interface --module ./expo-settings -w
--module 选项(简写 -m)是模块根目录的路径。运行该命令后,你应该会在模块包中看到 3 个新生成的文件。
expo-settings/src/ExpoSettings.types.ts
expo-settings/src/ExpoSettingsModule.ts
expo-settings/src/index.ts
现在让我们看看生成了什么。
expo-settings/src/ExpoSettings.types.ts
import { ViewProps } from 'react-native' ;
import { NativeModule } from 'expo' ;
export enum Theme {
light,
dark,
system,
}
*.types.ts 文件包含模块中定义的所有类型。在我们的例子里,我们只声明了一个 Theme 枚举,它已经被正确放入该文件。不过需要注意的是,与教程中的 ExpoSettings.types.ts 不同,事件类型并没有被生成。expo-type-information 工具是新的,而且很强大,但并不是每个选项都已经实现了,模块事件就是其中之一。
expo-settings/src/ExpoSettingsModule.ts
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 文件包含原生模块类的声明,并导出该模块实例。请注意,与前一个文件类似,这里也没有定义事件。
expo-settings/src/index.ts
export type * from './ExpoSettings.types' ;
export { default as ExpoSettings } from './ExpoSettingsModule' ;
这个 index 文件与示例相比差异更大。我们没有把每个模块方法包装成单独的函数,而是选择直接重新导出模块对象。