React TypeScriptでの型指定されたuseStateフックの使い方:初心者向け基礎解説
Reactで関数型コンポーネントを書く際に、最も基本的でよく使われるフックがuseStateです。
しかし、ReactとTypeScriptを組み合わせて使う場合、useStateフックで状態の型を宣言する方法に混乱することがあるかもしれません。
実際、ReactのフックはTypeScriptのジェネリック型を大いに利用しています。
そのため、まずはジェネリック型について理解していることが重要です。
この記事では、「ブール値」、「文字列」、「数値」、「配列」、「オブジェクト」の型の宣言方法について解説します。
これらの型について理解を深めることで、useStateフックで状態の型を宣言する方法についても理解を深めることができます。
ただし、この記事では、TypeScriptの基礎についてある程度の知識があることを前提としています。
それを踏まえた上で、以下に進んでいきましょう。
TypeScriptジェネリック型とは?
React/TypeScriptでは、あらゆる種類の型を受け入れる必要があるため、ジェネリックコンポーネントを作成することが必要になることがあります。
ジェネリックを理解するには、まず標準のTypeScriptの型とJavaScriptのオブジェクトを比較することから始める必要があります。
JavaScriptのオブジェクトは値を持っているだけで、TypeScriptの型はその値の型を示すものであるという点が異なります。
例えば、以下のようなオブジェクトと型がある場合です。
// JavaScript object const Store = { name: 'bar', status: 'open', }; // TypeScript type type Store = { name: string; status: string; };
上記の通り、非常に近いです。
この場合、型定義には"name"
と"status"
の2つのプロパティがありますが、"status"
には具体的な値が何が入っているのかは分かりません。
そこで、"status"
にはあらかじめ定義された値のみが入ることが期待される場合、下記のように型を定義することができます。
type StoreType = { name: string; status: 'open' | 'close'; };
しかし、実際にはあらかじめ定義された値を把握しているわけではない場合もあります。
このような場合には、ジェネリック型を使用することで型を動的に変更することができます。
下記は、ジェネリック型を使用して上記の例を再現した場合のコードです。
type StoreType<StatusOptions> = { name: string; status: StatusOptions; };
上記のように、"StatusOptions"
は型引数または型変数と呼ばれ、"StoreType"
はジェネリック型と呼ばれます。
ここでは、"status"
プロパティの値の型を動的に変更することができます。
例えば、以下のように使用することができます。
const store: StoreType<'open' | 'close' | 'hours'>;
これにより、"status"
プロパティに"hours"
というオプションを追加することができます。
また、ジェネリック型を使用することで、引数と戻り値の型を動的に変更することもできます。
下記は、ジェネリック型を使用した関数の例です。
function child<StatusType>(status: StatusType): StatusType { return status; }
上記の例では、任意の型である"StatusType"
を取り引数として受け取った値をそのまま返しています。
この関数にはジェネリック型が使われており、引数と戻り値の型が動的に変更されることができます。
例えば、下記のように使用することができます。
const str = child<string>('hello'); const num = child<number>(10); console.log(str); // "hello" console.log(num); // 10
この場合、"child"
関数は引数として渡された型に応じて、戻り値の型を変更することができます。
ジェネリック型は、特定の型に依存せず、汎用的な関数やクラスを作成する際に役立ちます。
また、型の安全性を高めることができるため、コードの品質を向上させることができます。
次に、ジェネリック型を使用してReactコンポーネントを作成する方法を見ていきましょう。
Reactでは、コンポーネントを再利用するためのパターンが非常に一般的であり、ジェネリックを使用して汎用的なコンポーネントを作成することができます。
以下は、ジェネリックを使用して作成された簡単なReactコンポーネントの例です。
import React from 'react'; type Props<T> = { data: T[]; render: (item: T) => React.ReactNode; }; function List<T>({ data, render }: Props<T>): React.ReactElement { return <ul>{data.map(render)}</ul>; }
この例では、Propsという名前のジェネリック型を使用しています。
Propsは、データとレンダリング関数の2つのプロパティを持つオブジェクト型です。
データはジェネリック型T
の配列であり、レンダリング関数はT型
のアイテムを受け取り、Reactノードを返す関数です。
List
関数は、ジェネリック型T
を使用してPropsを受け取り、ul
要素を返します。
このList
コンポーネントを使用する例を見てみましょう。
import React from 'react'; type User = { id: number; name: string; }; function App() { const users: User[] = [ { id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Bob' }, ]; return ( <div> <h1>User List</h1> <List data={users} render={(user) => <li key={user.id}>{user.name}</li>} /> </div> ); }
この例では、User
という名前の型を定義し、3つのUser
オブジェクトを含む配列を作成します。
次に、List
コンポーネントを呼び出し、data
プロパティにusers
配列を渡し、render
プロパティに、各ユーザーの名前を表示するli
要素を返す関数を渡します。
これにより、リストが表示されます。
以上が、React/TypeScriptでジェネリックコンポーネントを作成する方法の簡単な例です。
注意点がございます。
React/TypeScriptでジェネリックコンポーネントを作成する際に、アロー関数を使用する場合には注意が必要です。
JSXの場合、通常の関数のようにうまく処理できないため、いくつかの解決策があります。
例えば、以下のようにジェネリック型パラメータを指定し、戻り値の型をジェネリック型にすることができます。
const Child = <StatusType,>(status: StatusType): StatusType => { return status; };
また、下記のように 「extends unknown 」を使って型パラメータを制限することもできます。
const Child = <StatusType extends unknown>(status: StatusType): StatusType => { return status; };
ただし、JSXではこの方法はうまく動作しません。
そのため、前者のオプションを使用することをお勧めします。
この場合、ジェネリック型パラメータの後ろにカンマを入れる必要があります。
通常のTypeScriptでは、この解決策を使用する必要はありません。
この問題はTSXと呼ばれる、ReactとTypeScriptの統合に関する問題です。
以上のように、React/TypeScriptでジェネリックコンポーネントを作成する際には、JSXでうまく動作するように注意する必要があります。
useStateでプリミティブ型の使用
TypeScriptを使用すると、Reactアプリケーションの状態の型を宣言できます。
状態を保存するためにReactで提供されているuseStateフックは、「ブール値」、「文字列」、「数値」などのプリミティブ型をサポートしています。
これらの型は、TypeScriptによって推測されるため、明示的に指定する必要がない場合があります。
例えば、useStateフックを使用して文字列を保存する状態を宣言する場合、下記のように書くことができます。
const [name, setName] = useState('Taro');
同様に、ブール値を保存する状態を宣言する場合は、下記のように書くことができます。
const [open, setOpen] = useState(false);
状態に数値を保存する場合は、下記のように書くことができます。
const [count, setCount] = useState(0);
必要に応じて、型を指定することもできます。
例えば、ブール値を保存する状態を宣言する場合は、下記のように書くことができます。
const [disabled, setDisabled] = useState<boolean>(false);
このように、useStateフックは、Reactコンポーネントのマウント時に一度だけトリガーされるuseEffectフックに設定されるプリミティブ型の保存に使用されます。
下記の例では、useStateフックを使用して、「文字列」、「数値」、「ブール値」を保存する状態を宣言し、それらをuseEffectフックで初期化しています。
const App = () => { const [name, setName] = useState<string>(""); const [age, setAge] = useState<number>(0); const [isOpen, setIsOpen] = useState<boolean>(false); useEffect(() => { setName("Taro"); setAge(30); setIsOpen(false); }, []); return ( <div> <div>name : {name}</div> <div>Age : {age}</div> <div>public : {isOpen ? 'Yes' : 'No'}</div> </div> ); };
See the Pen React Rest API axios by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
このように、TypeScriptを使用することで、Reactアプリケーションの状態の型を宣言し、コンポーネントの安全性と可読性を向上させることができます。
また、状態を管理するために、オブジェクトリテラルの形式で初期値を定義することができます。
また、TypeScriptを使用する場合は、状態の型を指定することができます。
これにより、TypeScriptの静的型チェック機能を利用して、タイプミスや不正な操作を防止することができます。
例えば、下記のようなユーザーオブジェクトを定義する場合、状態の型をUser
インターフェースとして指定することができます。
interface User { name : string ; age : number ; isOpen : boolean ; }; const App = () => { const [state, setState] = useState<User>({ name : "", age : 0, isOpen : false }); useEffect(() => { setState({ name : "Taro", age : 30, isOpen : false }); }, []); return ( <div> <div>name : {state.name}</div> <div>Age : {state.age}</div> <div>public : {state.isOpen ? 'Yes' : 'No'}</div> </div> ); };
See the Pen React Rest API axios by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
また、useStateフックの「setState」関数を使用する場合、オプションフィールドを設定することができます。
その際には、型アサーションを使用して、型の整合性を確保する必要があります。
例えば、下記のようにオプションフィールドを設定する場合、型アサーションを使用してUser
型としてキャストする必要があります。
setState({ ...state, ...{ name: "Taro", isOpen: false } as unknown as User });
ここで、年齢フィールドは初期値のままであり、デフォルトの値が使用されます。
オブジェクトのオーバーライドには、スプレッド演算子(...)が使用されます。
TypeScriptは、初期値の型から型を推論することができますが、初期値がnull
の場合、型の推論はnull型
になります。
そのため、明示的に型を指定する必要があります。
例えば、下記のgreeting
がnull
か「文字列」のどちらかであることを示す共用体型ですが、常にnull
であるということになってしまいます。
const [greeting, setGreeting] = useState<null>(null);
そのため、後でgreeting
に「文字列」を設定する場合などには、他の型であることをTypeScriptに伝える必要があります。
その場合は、下記のように型アノテーションを使用して、greeting
がnull
か「文字列」のどちらかであることを明示します。
const [greeting, setGreeting] = useState<string | null>(null);
このように、プリミティブ以外の型(オブジェクトの複合型、null型、共用体型など)を扱う場合、TypeScriptが型の自動推論を行うとは限りません。
そのため、型の明示的な指定が必要になる場合があります。
型の推論が機能しない場合は、型アサーションよりも型引数に依存する必要があります。
下記は、型引数を使用してgreeting
の型を明示する例です。
const [greeting, setGreeting] = useState<Greeting>('Hello');
以上のように、型の推論に依存できる場合と依存できない場合があります。
適切な場面で型アサーションや型引数を使用することで、型の明示性を高め、コードの可読性や信頼性を向上させることができます。
また、useStateフックを利用する際には、状態の型を適切に指定することが、エラーを減らし、開発効率を向上させるために重要なポイントです。
関数の型定義
Reactで開発する際に、親コンポーネントから子コンポーネントに状態を渡す場合、useStateフックのセッター関数をPropsとして渡す必要がある場合があります。
しかし、このセッター関数の型を理解するのに苦労することがあります。
この問題を解決するために、useStateフックの型を正しく指定する必要があります。
例えば、状態の型がboolean
であれば、セッター関数の型はDispatch<SetStateAction<boolean>>
になります。
この型を用いて、Propsの型を定義することができます。
import { Dispatch, SetStateAction } from "react"; import { useState } from "react"; const App = () => { const [disabled, setDisabled] = useState<boolean>(false) return( <div> <Child setDisabled={setDisabled} /> { disabled ? ( <h1>Hello!!</h1> ):(<div>Loading...</div>) } </div> ) } const Child = ({ setDisabled, }: { setDisabled: Dispatch<SetStateAction<boolean>> }) => { return <div> Child component <button onClick={() => setDisabled(true)}>Click me</button> </div> }
See the Pen React with Typescript useState Props by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
また、セッター関数の型がDispatch<SetStateAction<number[]>>
のように複雑な場合でも同様に型を指定することができます。
この場合は、Propsの型を「interface」や「typeエイリアス」で定義する必要があります。
import { Dispatch, SetStateAction } from "react"; import { useState } from "react"; const App = () => { const [count, setCount] = useState<number>(0); return( <div> <Child count={count} setCount={setCount} /> </div> ); }; type Props = { count: number; setCount: Dispatch<SetStateAction<number>>; }; const Child = ({ count, setCount }: Props) => { return( <div> {count} <button type="button" onClick={() => setCount(count + 1)} >Click me</button> </div> ); };
See the Pen React with Typescript セッター関数を子に渡す typeエイリアス by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
このように、useStateフックを正しく型指定することで、Propsの型も正確に定義することができます。
これにより、型エラーを防ぎながら安全に開発を進めることができます。
下記のように、Props内の関数setCount
には引数として数値が期待されているため、Propsの型には明示的に数値型が指定されています。
type Props = { count: number; setCount: (num: number) => void; };
再利用可能なコンポーネントを作成する際に、Propsを「any型」として定義することは避けるべきです。
なぜなら、型安全性が失われるためです。
下記の例では、Propsのitems
プロパティに「any[]型」を指定し、onClickプロパティに(item: any, selectedIndex: number) => void型
を指定しています。
<Child items={anyArray} onClick={selectHandler} /> interface Props<T> { items: any[]; selectedItem: any; onClick: (item: any, selectedIndex: number) => void }
このように「any型」を使用すると、TypeScriptコンパイラや「IDE/Editor」は、onClickがどの型のパラメータを返すのか、selectedItem
がどの型のデータを受け入れるのかを推測できません。
代わりに、ジェネリック型を使用することを推奨します。
下記のように、Propsにジェネリック型を導入して、型パラメータTを使用して、items
とselectedItem
プロパティの型を指定します。
interface Props<T> { items: T[]; selectedItem: T; onClick: (item: T, selectedIndex: number) => void }
ReactのuseContext APIを使用する場合、Propsを使用せずに子コンポーネントにデータを渡すことができます。
この場合、TypeScriptはuseStateの初期値を使用して型を推論します。
ただし、状態が単純なプリミティブ型ではない場合は、型推論が期待通りに機能しないことがあります。
オブジェクトや配列などの複雑な値をuseStateで保存する場合、useStateフックにジェネリック型を指定する必要があります。
それらを簡単な例で見ていきましょう。
useStateで配列として入力する
ReactのTypeScriptにおいて、状態を管理するためにuseStateフックを使用することができます。
配列として状態を管理する場合、useStateのジェネリック型を使用して、配列内に含まれる要素の型を指定する必要があります。
例えば、下記のようにuseStateを使用することができます。
const [array, setArray] = useState<string[]>([]);
上記の例では、useStateのジェネリック型にstring[]
を渡し、空の配列で初期化しています。
配列に新しい要素を追加する場合は、JavaScriptのSpread演算子(…)を使用して、古い配列と新しい要素を組み合わせて、新しい配列を作成し、それをuseStateのセッター関数に渡すことで、状態を更新することができます。
<button onClick={() => setArray(prevArray => [...prevArray, 'Hello!!'])}> Click me </button>
See the Pen React with Typescript useState 配列 by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
上記の例では、配列の要素として、文字列'Hello!!'
を追加するボタンを作成しています。
ボタンがクリックされると、setArray
関数が呼び出され、前の配列の要素を受け取り、新しい要素を追加した配列を返します。
これにより、配列の状態が更新され、画面に反映されます。
状態変数の型を推論する場合は、型注釈なしで配列を宣言し、useStateに空の文字列を1つでも渡すことで、TypeScriptが状態変数の型を推測できます。
const [array, setArray] = useState([' ']);
上記の例では、useStateに空の「文字列」を渡して配列を初期化しています。
このようにすることで、TypeScriptは状態変数の型を推測し、string[]
として型付けされた配列が作成されます。
状態配列に異なる型の値を追加しようとすると、型チェックでエラーが発生するため、注意が必要です。
const [array, setArray] = useState<string[]>([]); setArray(prevArray => [...prevArray, 30]);
上記の例では、string[]
として型付けされた配列に数値を追加しようとしていますが、型チェッカーがエラーをスローします。
また、APIからデータを取得する場合は、非同期呼び出しが成功する前にデフォルトのnull
値を持つ必要があるため、useStateのジェネリック型に、データのインターフェースとして定義された型とnull
のユニオン型を渡すことが一般的です。
たとえば、下記のように、Post
というインターフェースを定義し、「Post型」と「null型」を「ユニオン型」で渡します。
interface Post { name: string; age: number; content: string; } const [count, setCount] = useState<Post | null>(null);
したがって、配列に追加する要素の型については、useStateのジェネリック型で指定する必要があります。
useStateでオブジェクトを型定義
Reactで状態を管理するために、useStateフックが使用されます。
useStateフックを使用する際には、ジェネリックを使用して、状態の型を指定することができます。
例えば、次のような状態を定義する場合、useStateフックとジェネリックを使用することができます。
const [user, setUser] = useState<{name: string; age: number}>({ name: '', age: 0, });
See the Pen React with Typescript useState オブジェクト by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
ここで、useStateフックにオブジェクトの初期値として渡されたオブジェクトは、name
とage
という2つのプロパティを持ち、name
プロパティは「文字列型」でage
プロパティは「数値型」であることを示しています。
このように、useStateフックを使用することで、state
変数には指定された型の「key」と「value」ペアのみを設定することができます。
ただし、すべてのプロパティに初期値を設定したくない場合は、オプションプロパティを使用することもできます。
下記の例では、age
プロパティをオプションプロパティとして設定しています。
const [user, setUser] = useState<{ name: string; age?: number; }>({ name: '', });
この場合、age
プロパティはオプションであり、「undefined」値または「数値型」の値を持つことができます。
これにより、状態オブジェクトを初期化する際に、age
プロパティの初期値を指定する必要がなくなります。
以上のように、useStateフックを使用する際には、ジェネリックを使用して正しい型を指定することが重要であり、必要に応じてオプションプロパティを使用することもできます。
また、オブジェクトに事前に設定するプロパティが全て明確でない場合、TypeScriptでは {[key: string]: any}
の構文を使用します。
この構文は、「文字列」のインデックスを使用して、オブジェクトの任意の型の値を取得できることを示します。
たとえば、次のように書くことができます。
const App = () => { const [user, setUser] = useState<{[key: string]: any}>({}); useEffect(() => { setUser({ name: 'Taro', age: 30, }); }, []); return ( <div> <h2>Name: {user.name}</h2> <h2>Age: {user.age}</h2> </div> ); };
また、オブジェクトのプロパティを複数の型のいずれかに設定したい場合は、「ユニオン型」を使用することができます。
たとえば、次のように書くことができます。
type User = { name: string; age: string | number; }; const App = () => { const [user, setUser] = useState<User>({ name: '', age: 0, }); useEffect(() => { setUser({name: 'Taro', age: 30}); }, []); return ( <div> <h2>Name: {user.name}</h2> <h2>Age: {user.age}</h2> </div> ); };
See the Pen React Rest API axios by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
ここで、useStateフックが、ジェネリックに渡される型をTypeScriptの「typeエイリアス」か「インターフェース」として抽出します。
このように書くことで、特に大きなオブジェクトを扱う場合に、構文がはるかに読みやすくなります。
以上が、TypeScriptにおいて、オブジェクトのプロパティが事前にすべてわからない場合や、複数の型のいずれかを設定する場合に、よく使用される方法です。
最後に
TypeScriptを使用することは、React.jsまたはReact Nativeプロジェクトをよりスケーラブルにし、より高品質なコードを書くための素晴らしい選択肢です。
普段のプリミティブ値は、多くの場合、型推論に頼ることができます。
そのため、TypeScriptは、useStateのようなフックの型を自動的に推測することができます。
これにより、多くの場合、ユーザーは特に何もする必要がありません。
ただし、TypeScriptが型を推測できない場合は、「ジェネリックパラメータ」を使用して、コンパイラをサポートすることができます。
また、自己記述的なコードを必要とする場合は、「型引数」を使用して明示的に型を指定することができます。
ReduxとTypeScriptを組み合わせることで、より複雑な状態を処理することができます。
ジェネリックと型推論機能は、フックを型安全に保つための最適なツールであるため、これはTypeScriptの最大の強みの1つです。
TypeScriptの機能を使用して、コンポーネントをジェネリックにすることは、再利用可能でクリーンなコードを保つための良い選択肢です。
この方法を使用することで、あなたとあなたのチームはよりスケーラブルなReact.jsまたはReact Nativeプロジェクトを作成し、維持することができます。
本日は以上となります。
最後に、この記事が役に立った場合は、他の方にも共有していただけると幸いです。