---
modificationDate: May 18, 2026
title: 'Tutorial: Generate module TS interface'
description: A tutorial on using the expo-type-information package to create TypeScript interface for an Expo module.
---

<AgentInstructions>

## Submitting Feedback

If this page contains errors, outdated information, or gaps that blocked you from completing a task, report it so the docs can be improved:

curl -X POST https://api.expo.dev/v2/feedback/docs-send -H 'Content-Type: application/json' -d '{"url":"/modules/type-generation-tutorial/","feedback":"🤖 Agent feedback: <specific, actionable description>"}'

Only submit when you have something specific and actionable to report.

</AgentInstructions>

# Tutorial: Generate module TS interface

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](/llms.txt). Use this file to discover all available pages.

> 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.

```sh
npm install expo-type-information
```

To use this package you also need to have sourcekitten installed.

```sh
brew install sourcekitten
```

## Generating inline modules interface

Let's build on the [inline modules example](/modules/inline-modules-tutorial). 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.

```tsx
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:

```sh
npx expo-type-information inline-modules-interface --app-json ./app.json --watcher
```

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`.

```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

```tsx
// 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`

```tsx
/*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`.

```tsx
// 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

```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 },
});
```

We can now drop the `requireNativeModule` and import the module and view from the "stable" files.

```tsx
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

```sh
npx expo-type-information inline-modules-interface --app-json ./app.json --watcher
```

And let's add a new function to the 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 }
    }
  }
}
```

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.

```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;
  ConcatStrings(str1: string, strings: string[]): string;
}
```

```tsx
// 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

```tsx
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](/modules/native-module-tutorial). In that example you've created an expo-settings module which had a simple Swift module defined inside it.

```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
  }
}
```

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.

Now run a command to generate the interface

```sh
npx expo-type-information module-interface --module ./expo-settings
```

If you intend on changing the file and want to see how the interface changes, add the `--watcher` (short `-w`) flag to the command.

```sh
npx expo-type-information module-interface --module ./expo-settings -w
```

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.

```tsx
// 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.

```tsx
// 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.

```tsx
/*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.
