migrate-to-strict-api について
React NativeプロジェクトをStrict TypeScript APIへ移行する手順を示すスキルです。前提条件の確認、深いインポートや型の修正を行い、型チェックとテストで結果を検証します。
主な機能
- 01React Nativeの前提条件確認
- 02Strict APIの有効化
- 03深いインポートの書き換え
- 04TypeScriptエラーの解消
- 05型チェックとテストの実行
活用シーン
- 01React NativeプロジェクトのStrict API移行
- 02移行に伴う型エラーの特定と修正
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ react-native-community/ skills --skill migrate-to-strict-apiインストール後、Claude Code で/migrate-to-strict-apiを入力して読み込まれたか確認します。
同梱ファイルreferences/library-compatibility.md
詳細
Strict TypeScript API への移行
React Native プロジェクトを Strict TypeScript API に移行します。型は自動生成され、react-native のルートエクスポートに限定されます。Strict API は React Native 0.87 以降のデフォルトであり、0.80~0.86 ではオプトイン形式のプレビューです。
この移行が影響するのは TypeScript の解析のみです。バンドルや実行時の動作には変更がないため、安全に段階的に適用でき、リリース時のリスクもありません。
実行方法
/migrate-to-strict-api
手順
以下の各手順を順番どおりに実行してください。手順を省略しないでください。
1. 前提条件を確認する
package.jsonを読み、react-nativeのバージョンが 0.80 以上であることを確認します。そうでない場合は中止し、まずアップグレードするようユーザーに伝えてください。バージョンが 0.87 以上かどうかも記録します。これは手順 3 と 5 の判断に使います。tsconfig.jsonが存在することを確認します。node_modules/react-native/types_generated/index.d.tsが存在することを確認します。存在しない場合は、先にパッケージマネージャーでインストールを実行するようユーザーに伝えてください。
2. 依存関係を更新する
一部のライブラリは、生の TypeScript ソースを含んでおり、ユーザーのプロジェクトの一部として型チェックされます。最も一般的なのは、セットアップファイルからインポートされる Jest モックのエントリーポイントです。Strict API では、通常 TS2307: Cannot find module 'react-native/Libraries/...' など、ユーザーが自身のコードで修正できない node_modules 内のエラーが発生します。
skipLibCheckが有効であることを確認します(@react-native/typescript-configでは有効に設定されています)。無効にしないでください。- この移行のどの時点でも、型エラーが
node_modules/<package>内で発生した場合は、まずそのパッケージを更新し、Strict API への互換性修正がリリースされていないか確認してください。修正済みの既知のバージョンは references/library-compatibility.md に記載されています。 - 修正済みのリリースがない場合は、インポートされたサブパスを型なしのスタブにリダイレクトしてローカルで修正し、ライブラリに互換性の問題を報告するようユーザーに伝えてください。
{
"compilerOptions": {
"paths": {
"some-library/jest/mock": ["./untyped-module.d.ts"]
}
}
}
// untyped-module.d.ts
declare const anyExport: unknown;
export default anyExport;
インラインの @ts-ignore はこのケースでは機能しません。エラーはインポート箇所ではなく、ライブラリのファイル内で報告されるためです。
3. tsconfig.json で Strict API を有効にする
React Native 0.87 以上 — Strict API はデフォルトです。
compilerOptions.customConditionsに"react-native-legacy-deep-imports"が含まれている場合は、その項目を削除します(これは一時的なオプトアウト設定であり、今後のリリースで削除される予定です)。"react-native"はそのまま残してください。- それ以外の場合、設定の変更は不要です。
React Native 0.80~0.86 — オプトインします。
compilerOptionsに"customConditions": ["react-native", "react-native-strict-api"]を追加します。両方の項目が必要です。customConditionsは拡張された設定の値と結合されるのではなく、置き換えるため、"react-native"を省略すると@react-native/typescript-configが設定した標準の条件が削除され、それを前提とするパッケージのモジュール解決に支障が出ます。カスタム条件を有効にするには、プロジェクトで"moduleResolution": "bundler"(または"node16"/"nodenext")を使用する必要があります。
4. ディープインポートを修正する
4a. まず ESLint の自動修正を実行する
@react-native/eslint-plugin には no-deep-imports ルールがあり、既知のディープインポート(デフォルトインポートと型インポート)をルートインポートに書き換えるコード自動修正機能が備わっています。このプラグインがインストールされている場合(@react-native/eslint-config に同梱されています)は、プロジェクトの ESLint 設定でルールを有効にしてください。
{"@react-native/no-deep-imports": "error"}
その後、プロジェクトのソース全体に対して --fix を指定して ESLint を実行します。再発防止のため、以降もこのルールを有効にしておくことをおすすめします。
4b. 残りの手動置換
node_modules/react-native/types_generated/index.d.ts を読みます。このファイルは、Strict API で利用可能なすべてのエクスポートの信頼できる情報源です。各行は、内部のソースパスをエクスポート名に対応付けています。
export { default as Alert } from "./Libraries/Alert/Alert";
export type { AlertButton } from "./Libraries/Alert/Alert";
このファイルを解析して、次の 2 つのマッピングを作成します。
- 値のエクスポート:内部パス + 元の名前 → エクスポート名(例:
./Libraries/Alert/Alertの default →Alert) - 型のエクスポート:内部パス → エクスポートされる型名のリスト(例:
./Libraries/Alert/Alert→AlertType、AlertButton、...)
すべてのソースファイル(.ts、.tsx、.js、.jsx)を検索し、react-native/Libraries/... または react-native/src/... のサブパスからの残りのインポートを、マッピングを使って書き換えます。
| ディープインポートのパターン | 置換後 |
|---|---|
import Foo from 'react-native/Libraries/.../Foo' | import {Foo} from 'react-native' |
import type {FooProps} from 'react-native/Libraries/.../Foo' | import type {FooProps} from 'react-native' |
import {named} from 'react-native/Libraries/.../Foo' | import {named} from 'react-native' |
ファイルにすでに react-native のルートインポートがある場合は、重複して追加せず、そのインポートにまとめます。
Codegen のインポートは CodegenTypes 名前空間に移します。
// Before
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';
import type {Int32, WithDefault} from 'react-native/Libraries/Types/CodegenTypes';
// After
import {codegenNativeComponent, codegenNativeCommands} from 'react-native';
import type {CodegenTypes} from 'react-native';
// Usage changes: Int32 → CodegenTypes.Int32, WithDefault<...> → CodegenTypes.WithDefault<...>
InitializeCore は、ルートエクスポートのない副作用用エントリーポイントです。専用のサブパスに置き換えます(通常は Jest のセットアップファイルやカスタムエントリーポイントにあります)。
- import 'react-native/Libraries/Core/InitializeCore';
+ import 'react-native/setup-env';
ディープインポートのパスがマッピングに見つからない場合、そのパスは意図的に非公開です。ルート API に代替手段がないか確認してください。ユースケースをカバーするものがない場合は、ユーザーに報告し、フィードバックのスレッドを案内します。react-native/src/private/* のインポートも 0.87 では実行時に削除されることに注意してください。これらの使用箇所は、型だけを変更するのではなく、削除する必要があります。
4c. Jest の文字列モックはそのままにする
jest.mock('react-native/Libraries/...') や jest.requireActual(...) のパス文字列は書き換えないでください。Jest と Metro のモジュール解決に変更はないため、これらは引き続き動作し、TypeScript によるチェックも行われません。テストファイルがディープパスをモジュールとしてインポートする場合に限り、TypeScript エラーが発生します。ユースケースをカバーするルートエクスポートがない場合は、インポートを残し、明示的にエラーを抑制します。
// @ts-expect-error - React Native internal, untyped under the Strict API
import NativeAppState from 'react-native/Libraries/AppState/NativeAppState';
5. 型チェックを実行し、残りの型エラーを修正する
npx tsc --noEmit を実行します。次の既知のパターンを使って、エラーを繰り返し修正します。
コンポーネントの ref とインスタンスの型
コアコンポーネントは関数コンポーネントとして型付けされるようになったため、コンポーネント名をインスタンスの型として使えなくなりました(useRef<TextInput> はコンポーネント関数を参照するため、.focus() などのメソッドが存在しないように扱われます)。これが最もよくある阻害要因です。
- 0.87 以降では、専用の
*Instance型を使います。ref をサポートするコアコンポーネントはすべて、ViewInstance、TextInputInstance、ScrollViewInstance、FlatListInstance、... のような型をエクスポートします。ref をサポートしないコンポーネントにはインスタンス型がありません:InputAccessoryView、TouchableWithoutFeedback、experimental_LayoutConformance。 - 0.80~0.86では、
React.ComponentRef<typeof Component>を使います。同じインスタンス型を解決し、Strict API とレガシー型の両方で動作します。
// Before
const inputRef = useRef<TextInput>(null);
// After (0.87+) — preferred
const inputRef = useRef<TextInputInstance>(null);
// After (any version) — equivalent
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null);
コンポーネント名がインスタンス型として使われている箇所すべてに適用します。対象には、useRef/createRef の型引数、ラッパーコンポーネントの ref プロパティ(ref?: React.Ref<ViewInstance>)、React.forwardRef<View, Props> の型引数、インスタンスを保持する変数が含まれます。
関連する置換:
Animated.LegacyRef<View>は削除されました。通常の*Instance型を使います。*Instance型はAnimated.*コンポーネントのバリアントでもそのまま使えます。NativeMethods/NativeMethodsMixinは削除されました。HostInstanceまたは個別の*Instance型を使います。
*Static 型の削除
API 名自体が、値とその型の両方としてエクスポートされるようになりました:
// Before
import {Linking, LinkingStatic} from 'react-native';
function foo(linking: LinkingStatic) {}
// After
import {Linking} from 'react-native';
function foo(linking: Linking) {}
以前に同名の型が存在しなかったもの (InteractionManagerStatic、PixelRatioStatic、DevMenuStatic、KeyboardStatic、DevSettingsStatic、NativeModulesStatic) については、代わりに値の typeof を使用してください (例: typeof PixelRatio)。
完全なリスト: AlertStatic、ActionSheetIOSStatic、ToastAndroidStatic、InteractionManagerStatic、UIManagerStatic、PlatformStatic、SectionListStatic、PixelRatioStatic、AppStateStatic、AccessibilityInfoStatic、ImageResizeModeStatic、BackHandlerStatic、DevMenuStatic、ClipboardStatic、PermissionsAndroidStatic、ShareStatic、DeviceEventEmitterStatic、LayoutAnimationStatic、KeyboardStatic、DevSettingsStatic、I18nManagerStatic、EasingStatic、PanResponderStatic、NativeModulesStatic、LogBoxStatic、PushNotificationIOSStatic、SettingsStatic、VibrationStatic
Animated 型の変更
Animated ノードはジェネリックではなくなりました — ジェネリックな interpolate メソッドを持つ非ジェネリック型になりました。Animated.Value、Animated.ValueXY などから型パラメーターを削除してください。
非推奨の *Properties エイリアス
レガシーな *Properties エイリアスは削除されました。対応する *Props 型に名前を変更してください: ViewProperties → ViewProps、TextProperties → TextProps、ImageProperties → ImageProps など — さらに ImagePropertiesSourceOptions は ImageSourcePropType になります。
StyleSheet.absoluteFillObject
Strict API の一部ではありません。同等で、両方の API モードで動作する StyleSheet.absoluteFill に置き換えてください:
// Before
style={{...StyleSheet.absoluteFillObject, borderRadius: 12}}
// After
style={{...StyleSheet.absoluteFill, borderRadius: 12}}
useColorScheme() と 'unspecified'
戻り値の型に 'unspecified' が含まれなくなりました — この値は古い手動型では不正確に型付けされており、実行時には決して発生しませんでした。'dark' をチェックし、デフォルトで 'light' にしてください:
// Before — handles a case that cannot occur
const theme = scheme === 'unspecified' ? 'light' : scheme;
// After
const theme = scheme === 'dark' ? 'dark' : 'light';
オプションの props は type | undefined
すべてのオプションの prop は type | undefined として型付けされるようになりました。React Native の props を再宣言するラッパー型は、一致させるために型を広げる必要がある場合があります。
内部ヘルパー型
RecursiveArray、RegisteredStyle、Falsy、WithAnimatedArray、WithAnimatedObject、その他の内部専用ヘルパーは react-native からアクセスできなくなりました。型定義をインライン化するか、代替手段を探してください。
残存していたコンポーネント props の削除
古い型定義にのみ存在していた props が削除されました — たとえば Text の lineBreakMode、ScrollView の scrollWithoutAnimationTo、transform 配列の外で宣言された transform スタイルなどです。これらの使用箇所を削除または置き換えてください。
6. 検証
- 再度
npx tsc --noEmitを実行してください。型エラーがゼロであることを確認してください。 - プロジェクトに Jest スイートがある場合は実行してください。モックはこの移行の影響を受けません。先に進む前に、新しい失敗があれば調査してください。
すべての変更の概要を提示してください:
- 変更されたファイル (件数)
- 書き換えられたディープインポート (件数)
- 修正された型エラー (カテゴリ別)
- ユーザーに通知する事項 (ルートに相当するものがないプライベート API、ライブラリの非互換性)
移行を中断する場合
ライブラリの互換性を解決できない、現在ユーザーが対応できる範囲を超える数のエラーが発生した、またはユーザーが延期を希望したなどの理由で移行を完了できない場合は、プロジェクトを一時的に従来の型に戻せます。
- React Native >= 0.87:
compilerOptionsで"customConditions": ["react-native", "react-native-legacy-deep-imports"]を設定します(手順 3 と同様に、両方のエントリーが必要です)。このオプトアウトは一時的なもので、今後のリリースで削除される予定です。 - React Native 0.80–0.86:
customConditionsから"react-native-strict-api"を削除し、以前の値に戻します。
どちらの API モードでも有効な、完了済みの作業は元に戻さないでください。後で再度移行を試みる際の前進となります。
- ディープインポートをルートインポートに書き換える
CodegenTypes名前空間の使用(Strict API が有効でない場合もエクスポートされます)react-native/setup-envReact.ComponentRef<typeof Component>形式で記述した ref 型
例外は *Instance ref 型です。これは Strict API でのみ使用できるため、手順 5 の後に中断する場合は、同等の React.ComponentRef<typeof Component> 形式に変換してください。最初から中断する可能性が高い場合は、全体を通してこの形式を使用することをおすすめします。
0.87 以降で移行を中断する場合は、何が障害になったのかをオプトアウトに関するディスカッションスレッドで共有するようユーザーに促してください。