jotai × TypeScript 型推論を極める実戦のための環境設定術
React の状態管理ライブラリとして注目を集める Jotai ですが、TypeScript との組み合わせで真価を発揮します。型推論がしっかり効くことで、開発体験が劇的に向上し、バグを未然に防げるのです。
しかし、実際のプロジェクトでは「型が any になってしまう」「推論が効かない」といった問題に直面することも少なくありません。本記事では、Jotai と TypeScript の型推論を最大限に活用するための環境設定術を、実戦的な視点から徹底解説していきます。
背景
Jotai が選ばれる理由
Jotai は Recoil にインスパイアされた軽量な状態管理ライブラリで、atomic なアプローチを採用しています。Redux のような大規模なボイラープレートを必要とせず、シンプルな API で状態管理を実現できるのが特徴です。
特に TypeScript との相性が良く、型推論が自動的に効くように設計されているため、型定義を書く手間が大幅に削減されます。
TypeScript 型推論の重要性
TypeScript の型推論は、開発者が明示的に型を書かなくても、コンパイラが自動的に型を推測してくれる機能です。これにより以下のメリットが得られます。
- コードの記述量が減り、可読性が向上する
- IDE の補完機能が強力に働き、開発速度が上がる
- 型の不整合によるバグを事前に検知できる
以下の図は、Jotai と TypeScript の型推論がどのように連携するかを示しています。
mermaidflowchart TB
atom["atom 定義"] -->|型情報| infer["TypeScript 型推論"]
infer -->|自動推論| hook["useAtom フック"]
hook -->|型安全な値| comp["React コンポーネント"]
comp -->|型チェック| ide["IDE 補完・エラー検出"]
tsconfig["tsconfig.json"] -.->|コンパイラ設定| infer
deps["依存関係バージョン"] -.->|型定義整合性| infer
上図のように、atom の定義から始まる型情報の流れが、適切な環境設定により IDE のサポートまで一貫して機能します。
Jotai の型システムの仕組み
Jotai は内部的にジェネリクスを活用して、atom の値の型を保持しています。基本的な atom の型定義は次のようになっています。
typescript// Jotai 内部の型定義(簡略版)
type Atom<Value> = {
read: (get: Getter) => Value;
write?: (get: Getter, set: Setter, update: any) => void;
};
このジェネリクスにより、atom を定義した時点で値の型が確定し、その型情報が useAtom や useAtomValue などのフックに自動的に伝播します。
課題
型推論が効かないケース
実際の開発現場では、以下のような問題が頻発します。
問題 1: 初期値が null や undefined の場合
typescriptimport { atom } from 'jotai';
// ❌ 型が null のまま推論されてしまう
const userAtom = atom(null);
// ❌ 後から値を設定しても型エラー
const handleLogin = () => {
// Type 'User' is not assignable to type 'null'
set(userAtom, { id: 1, name: 'John' });
};
この場合、TypeScript は初期値から型を推論するため、userAtom の型が Atom<null> と判断されてしまいます。
問題 2: 複雑な派生 atom での型喪失
typescript// ❌ 型推論が効かず any になってしまう
const derivedAtom = atom((get) => {
const data = get(someAtom);
// 複雑な変換処理
return someComplexTransform(data);
});
複雑な変換処理を含む派生 atom では、戻り値の型が自動推論されず、any 型になってしまうことがあります。
問題 3: 非同期 atom の型エラー
typescript// ❌ Promise の型が正しく推論されない
const fetchUserAtom = atom(async (get) => {
const response = await fetch('/api/user');
return response.json(); // any 型になる
});
非同期処理を含む atom では、Promise の解決値の型が失われがちです。
環境設定の不備による問題
TypeScript の設定が適切でない場合、Jotai の型推論が正しく機能しません。
以下の図は、環境設定の不備がどのように型推論の問題を引き起こすかを示しています。
mermaidflowchart TD
config_issue["設定の不備"] --> strict["strict モード無効"]
config_issue --> target["target が古い"]
config_issue --> libs["lib 設定不足"]
strict --> any_type["暗黙の any 許可"]
target --> compat["型定義の不整合"]
libs --> missing["型サポート欠如"]
any_type --> error["型エラー未検出"]
compat --> error
missing --> error
error --> bug["実行時エラー・バグ"]
図から分かるように、設定の不備は複数の経路を通じて最終的なバグにつながります。
主な問題点は以下の通りです。
strictモードが無効で、暗黙のany型が許容されるtargetが古く、最新の型定義との互換性がないmoduleResolutionが適切でなく、型定義ファイルが正しく解決されない
解決策
TypeScript コンパイラオプションの最適化
Jotai で型推論を最大限に活用するには、tsconfig.json の設定が極めて重要です。以下の設定を推奨します。
基本設定: compilerOptions
json{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler"
}
}
target は ES2020 以上に設定することで、最新の JavaScript 機能と型定義のサポートを得られます。
moduleResolution は bundler または node16 を指定すると、現代的なモジュール解決が可能になります。
Strict モードの有効化
型安全性を確保するため、以下の strict 系オプションをすべて有効化します。
json{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
strict: true だけでも大半が有効になりますが、明示的に記載することで設定の意図が明確になります。
型チェックの強化オプション
さらに厳密な型チェックを行うため、以下のオプションも追加しましょう。
json{
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
特に noUncheckedIndexedAccess は、配列やオブジェクトのインデックスアクセス時に undefined の可能性を考慮させるため、Jotai での状態管理において有用です。
完全な tsconfig.json の例
上記をまとめた推奨設定は以下の通りです。
json{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"allowJs": false,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
この設定により、Jotai の型推論が正確に機能する土台が整います。
パッケージバージョンの整合性確保
Jotai と TypeScript、React のバージョンには相性があります。型定義の互換性を保つため、以下のバージョン組み合わせを推奨します。
| # | パッケージ | 推奨バージョン | 理由 |
|---|---|---|---|
| 1 | jotai | ^2.6.0 以上 | 最新の型定義サポート |
| 2 | typescript | ^5.3.0 以上 | 最新の型推論機能 |
| 3 | react | ^18.2.0 以上 | React 18 の型定義 |
| 4 | @types/react | ^18.2.0 以上 | React の型定義 |
package.json の設定例
json{
"dependencies": {
"jotai": "^2.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"typescript": "^5.3.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0"
}
}
これらのバージョンを使用することで、型定義の不整合を回避できます。
インストールコマンド
Yarn を使用する場合、以下のコマンドで依存関係をインストールします。
bashyarn add jotai react react-dom
開発用の依存関係も追加します。
bashyarn add -D typescript @types/react @types/react-dom
IDE 設定の最適化
VSCode を使用している場合、以下の設定で TypeScript の型チェック体験が向上します。
.vscode/settings.json
json{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.preferences.quoteStyle": "single",
"typescript.updateImportsOnFileMove.enabled": "always",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
}
}
typescript.tsdk により、プロジェクトローカルの TypeScript を使用し、バージョン整合性が保たれます。
推奨拡張機能
以下の VSCode 拡張機能をインストールすると、開発効率がさらに向上します。
| # | 拡張機能 | 用途 |
|---|---|---|
| 1 | ESLint | コード品質チェック |
| 2 | Prettier | コードフォーマット |
| 3 | TypeScript Importer | 自動インポート補完 |
具体例
型安全な atom 定義パターン
ここからは、実際のコードで型推論を最大限に活用する方法を見ていきましょう。
パターン 1: プリミティブ型の atom
最もシンプルなケースです。初期値から型が自動推論されます。
typescriptimport { atom } from 'jotai';
// number 型として推論される
const countAtom = atom(0);
// string 型として推論される
const nameAtom = atom('John');
// boolean 型として推論される
const isLoadingAtom = atom(false);
このように、プリミティブ型の場合は明示的な型定義は不要です。
パターン 2: オブジェクト型の atom
オブジェクトの場合も、初期値から型が推論されます。
typescript// User 型が自動推論される
const userAtom = atom({
id: 1,
name: 'John',
email: 'john@example.com',
});
// 型: Atom<{ id: number; name: string; email: string }>
ただし、より厳密な型定義が必要な場合は、インターフェースを定義します。
パターン 3: 型定義を明示する atom
null や undefined を初期値とする場合、ジェネリクスで型を明示します。
typescriptinterface User {
id: number;
name: string;
email: string;
}
// ✅ User | null 型として定義
const userAtom = atom<User | null>(null);
これにより、後から User オブジェクトを設定できるようになります。
typescript// コンポーネント内での使用
const [user, setUser] = useAtom(userAtom);
const handleLogin = () => {
// ✅ 型安全に設定可能
setUser({
id: 1,
name: 'John',
email: 'john@example.com',
});
};
パターン 4: 配列型の atom
配列の場合も同様に、初期値から型が推論されます。
typescriptinterface Todo {
id: number;
title: string;
completed: boolean;
}
// ✅ Todo[] 型として推論される
const todosAtom = atom<Todo[]>([]);
空配列の場合は型が推論できないため、ジェネリクスで明示する必要があります。
派生 atom の型推論テクニック
派生 atom では、get で取得した値の型が自動的に推論されます。
シンプルな派生 atom
typescript// countAtom の値(number)から自動推論
const doubledCountAtom = atom((get) => {
const count = get(countAtom);
return count * 2; // number 型として推論
});
TypeScript は get(countAtom) が number を返すことを理解し、戻り値も number と推論します。
複数の atom を組み合わせる
typescriptconst fullNameAtom = atom((get) => {
const firstName = get(firstNameAtom); // string
const lastName = get(lastNameAtom); // string
return `${firstName} ${lastName}`; // string として推論
});
複数の atom を参照する場合も、それぞれの型が正しく推論されます。
フィルタリングやマッピング
配列を扱う派生 atom でも、型推論が効きます。
typescript// 完了していない Todo のみを抽出
const incompleteTodosAtom = atom((get) => {
const todos = get(todosAtom); // Todo[]
return todos.filter((todo) => !todo.completed); // Todo[] として推論
});
// Todo の件数
const todoCountAtom = atom((get) => {
const todos = get(todosAtom); // Todo[]
return todos.length; // number として推論
});
filter や map などの配列メソッドを使用しても、型情報は保持されます。
明示的な戻り値の型指定
複雑な変換処理では、戻り値の型を明示することで可読性が向上します。
typescriptinterface Summary {
total: number;
completed: number;
incomplete: number;
}
// ✅ 戻り値の型を明示
const todoSummaryAtom = atom<Summary>((get) => {
const todos = get(todosAtom);
return {
total: todos.length,
completed: todos.filter((t) => t.completed).length,
incomplete: todos.filter((t) => !t.completed).length,
};
});
型を明示することで、戻り値の構造が保証されます。
書き込み可能な atom の型安全性
読み取り専用でない atom では、write 関数の型も重要です。
基本的な書き込み可能 atom
typescriptconst userAtom = atom<User | null>(null);
// write 関数付きの atom
const loginAtom = atom(
null, // 読み取り値は使用しない
(get, set, newUser: User) => {
set(userAtom, newUser);
}
);
第 3 引数 newUser の型を明示することで、型安全な更新が可能になります。
複雑な更新ロジック
typescriptconst addTodoAtom = atom(
null,
(get, set, title: string) => {
const todos = get(todosAtom);
const newTodo: Todo = {
id: Date.now(),
title,
completed: false,
};
set(todosAtom, [...todos, newTodo]);
}
);
このパターンでは、title: string と型を指定することで、呼び出し側でも型チェックが機能します。
typescript// コンポーネント内
const [, addTodo] = useAtom(addTodoAtom);
const handleAdd = () => {
addTodo('New Task'); // ✅ string を渡す必要がある
// addTodo(123) // ❌ 型エラー
};
非同期 atom の型推論
非同期処理を含む atom では、Promise の型を正しく推論させる必要があります。
基本的な非同期 atom
typescriptinterface ApiUser {
id: number;
name: string;
email: string;
}
// ✅ Promise<ApiUser> として推論される
const fetchUserAtom = atom(
async (get): Promise<ApiUser> => {
const response = await fetch('/api/user');
const data: ApiUser = await response.json();
return data;
}
);
戻り値の型を Promise<ApiUser> と明示することで、型安全性が確保されます。
Suspense と組み合わせる
Jotai は React Suspense と統合されており、非同期 atom を自然に扱えます。
typescriptimport { Suspense } from 'react';
import { useAtomValue } from 'jotai';
function UserProfile() {
// user の型は ApiUser として推論される
const user = useAtomValue(fetchUserAtom);
return <div>{user.name}</div>;
}
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</Suspense>
);
}
Suspense により、Promise の解決を待つ間はフォールバック UI が表示されます。
エラーハンドリング付き非同期 atom
実践的には、エラーハンドリングも含めた実装が必要です。
typescriptconst fetchUserWithErrorAtom = atom(
async (get): Promise<ApiUser> => {
try {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const data: ApiUser = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch user:', error);
throw error;
}
}
);
エラーが発生した場合、React の Error Boundary でキャッチできます。
以下の図は、非同期 atom のライフサイクルと型の流れを示しています。
mermaidsequenceDiagram
participant Comp as コンポーネント
participant Hook as useAtomValue
participant Atom as 非同期 atom
participant API as API サーバー
Comp->>Hook: useAtomValue(fetchUserAtom)
Hook->>Atom: 値を要求
Atom->>API: fetch('/api/user')
alt 成功時
API-->>Atom: JSON レスポンス
Atom-->>Hook: Promise<ApiUser> 解決
Hook-->>Comp: ApiUser 型の値
else エラー時
API-->>Atom: エラーレスポンス
Atom-->>Hook: Promise reject
Hook-->>Comp: Error Boundary へ
end
図で理解できる要点:
- 非同期 atom は Promise を返し、Suspense が解決を待つ
- 成功時は型安全な値が取得でき、エラー時は Error Boundary でキャッチされる
- 型情報は API からコンポーネントまで一貫して保持される
atomWithStorage の型推論
Jotai の utilities には、localStorage と連携する atomWithStorage があります。
基本的な使い方
typescriptimport { atomWithStorage } from 'jotai/utils';
// ✅ string 型として推論される
const themeAtom = atomWithStorage('theme', 'light');
// ✅ User | null 型として明示
const storedUserAtom = atomWithStorage<User | null>(
'user',
null
);
初期値から型が推論されますが、複雑な型の場合はジェネリクスで明示します。
カスタムストレージの型定義
独自のストレージ実装を使用する場合、型定義が必要です。
typescriptimport { createJSONStorage } from 'jotai/utils';
interface CustomStorage<T> {
getItem: (key: string) => T | null;
setItem: (key: string, value: T) => void;
removeItem: (key: string) => void;
}
const customStorage = createJSONStorage<User>(
() => sessionStorage
);
const userAtom = atomWithStorage<User | null>(
'user',
null,
customStorage
);
ジェネリクスを使用することで、ストレージの型安全性も確保されます。
Family パターンの型推論
パラメータ化された atom を作成する family パターンでも、型推論が重要です。
atomFamily の使用
typescriptimport { atomFamily } from 'jotai/utils';
interface TodoState {
title: string;
completed: boolean;
}
// ✅ パラメータ(id: number)と戻り値の型が推論される
const todoAtomFamily = atomFamily((id: number) =>
atom<TodoState>({
title: '',
completed: false,
})
);
パラメータの型を明示することで、呼び出し側でも型チェックが機能します。
コンポーネントでの使用
typescriptfunction TodoItem({ id }: { id: number }) {
// todo の型は TodoState として推論される
const [todo, setTodo] = useAtom(todoAtomFamily(id));
return (
<div>
<input
value={todo.title}
onChange={(e) =>
setTodo({ ...todo, title: e.target.value })
}
/>
</div>
);
}
Family パターンにより、ID ごとに独立した状態を型安全に管理できます。
まとめ
Jotai と TypeScript を組み合わせた開発では、適切な環境設定が型推論の効果を最大化します。本記事で解説した内容を改めて整理しましょう。
環境設定のチェックリスト
以下の設定を確認することで、型推論が正しく機能する環境が整います。
| # | 項目 | 推奨設定 |
|---|---|---|
| 1 | TypeScript バージョン | 5.3.0 以上 |
| 2 | Jotai バージョン | 2.6.0 以上 |
| 3 | strict モード | 有効化 |
| 4 | noImplicitAny | 有効化 |
| 5 | strictNullChecks | 有効化 |
| 6 | moduleResolution | bundler または node16 |
| 7 | target | ES2020 以上 |
型推論を活かすコーディングパターン
実装時に意識すべきポイントは以下の通りです。
- 初期値から型が推論できない場合は、ジェネリクスで型を明示する
- 派生 atom では、複雑な変換処理の戻り値に型を指定する
- 非同期 atom では、Promise の解決値の型を明確にする
- 書き込み可能な atom では、update パラメータの型を定義する
- atomFamily などのユーティリティでも、パラメータの型を明示する
開発体験の向上
適切な環境設定により、以下の恩恵が得られます。
まず、IDE の補完機能が強力に働き、コーディング速度が向上します。次に、型エラーが開発時に検出されるため、実行時のバグが激減するでしょう。さらに、リファクタリング時の安全性が高まり、大規模な変更も安心して行えます。
Jotai の軽量さと TypeScript の型安全性を組み合わせることで、保守性の高い React アプリケーションを構築できるのです。
今後の学習ステップ
本記事で扱った環境設定をベースに、以下のステップで学習を深めていくことをお勧めします。
実際のプロジェクトに Jotai を導入し、状態管理を実装してみてください。次に、Jotai の公式ドキュメントを読み、より高度なパターンを学びましょう。また、TypeScript の型システムについても理解を深めると、さらに効果的な活用が可能になります。
型推論を極めることで、開発効率と品質の両立が実現できます。ぜひ本記事の設定を試し、快適な開発体験を手に入れてください。
関連リンク
articlejotai × TypeScript 型推論を極める実戦のための環境設定術
articleJotai のリアクティブ思考法:コンポーネントから状態を切り離す設計哲学
articleJotai 運用ガイド:命名規約・debugLabel・依存グラフ可視化の標準化
articleJotai のスケール特性を実測:コンポ数 × 更新頻度 × 派生深さのベンチ
articleJotai が再レンダリング地獄に?依存グラフの暴走を止める診断手順
articleJotai ドメイン駆動設計:ユースケースと atom の境界を引く実践
articleLodash の組織運用ルール:no-restricted-imports と コーディング規約の設計
articleRedis 7 の新機能まとめ:ACL v2/I/O Threads/RESP3 を一気に把握
articleLlamaIndex の Chunk 設計最適化:長文性能と幻覚率を両立する分割パターン
articleReact フック完全チートシート:useState から useTransition まで用途別早見表
articleLangChain × Docker 最小構成:軽量ベースイメージとマルチステージビルド
articlePython UnicodeDecodeError 撲滅作戦:エンコーディング自動判定と安全読込テク
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来