はじめに
業務でフロント側はReact(Next.js), TypeScriptを使っているのですが、配属当初の私はReactにもTypeScriptにも触れたことが無く、何が分からないのか分からない状態でした。
TypeScriptの基礎を学んでもReact特有の型定義があり戸惑いましたが、ようやく理解して書けるようになって来たな…ということで、辞書代わりにまとめておきます。
(誤りのご指摘・アドバイスがあればぜひお願いします!)
基礎編
まずは基本の型定義を一通り挙げます。
関数コンポーネント
関数コンポーネントでは、普通の関数と同じように引数(props)に型付けします。
戻り値はJSX要素を返せば型推論されるので、明らかな場合は型付けしなくてOKです。
※以下の例では、propsは分割代入の書き方で受け取るものとします。
引数(props)なしの場合
propsを受け取らない場合は、JavaScriptで記述するときと変わりません。
// ノーマルの関数コンポーネント(引数なしの場合)
const App = () => <div>なまえ</div>
ひとつのpropsを受け取る
// 引数に型付けする
const App = ({ name }: { name: string }) => <div>{name}</div>
// 型エイリアスを使っても良い
type Props = { name: string }
const App = ({ name }: Props) => <div>{name}</div>
型注釈を直接つけても、型エイリアスを使ってもOKです。
プロジェクト内で合わせておけば良いかと思います。
複数のpropsを受け取る
propsを複数受け取る場合は、型注釈を直接つけるよりも型エイリアスを付けた方が分かりやすいでしょう。
// 引数が複数なら、型エイリアスを使う方が見やすい
type Props = {
name: srting
onClick: () => void
children: ReactNode
}
propsの型いろいろ
以下のように色々な型の値をpropsとして受け取ることができます。
ここはほとんどTypeScriptの基本通りです。
// propsとして受け取る値の型定義色々
type Props = {
str: string // 文字列
num: number // 数値
bool: boolean // 真偽値
strArr: string[] // 配列
obj: { // オブジェクト
str: string
}
objArr: { // オブジェクトの配列
str: string
num: number
}[]
func: () => void // 関数
}
children
childrenはコンポーネントのタグで囲った子要素をpropsとして受け取るときに使います。
よく使うのはReactNodeで、これはコンポーネントタグで囲ったJSX要素をまるっと受け取る場合に使います。
// 共通レイアウトのコンポーネント
const MainLayout = ({ children }: { children: React.ReactNode }) => {
return (
<>
<Header />
<div>
{children}
</div>
<Footer />
</>
)
}
// 呼び出し側
const MenuPage = () => {
return (
<MainLayout>
// この中にページの内容を書く
</MainLayout>
)
}
もし特定の文字列や数値しか受け取らないというような場合は、ReactNodeではなく特定の型に制限することもできます。
// Buttonコンポーネント
const Button = ({ children }: { children: string }) => {
return (
<button>{children}</button>
)
}
// 呼び出し側
const App = () => {
return (
<>
// フォームの記述があるとする
<Button>送信</Button>
<Button>戻る</Button>
</>
)
}
ちなみにReactNodeは以下の通りのUnion型で定義されています。
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
propsのデフォルト値を指定する
コンポーネントの利用箇所ごとにpropsに渡す値を指定してもよいのですが、デフォルト値を指定することもできます。
よく指定される値がある時や、値が無かった場合の値を決めておきたい場合に有効です。
const Name = ({ name = "ゲスト" }: { name?: string }) => <div>{name}さん</div>
オプショナルなprops(undefined許容)
?
を付けることでオプショナルな型定義が出来ます。
以下のように書いた場合、name・emailの型はstring | undefined
となります。
type Props = {
name?: string
email?: string
}
この場合はundefinedの可能性を考慮してpropsの初期値を設定するか、値の有無による条件分岐を行います。
// nameは初期値を設定、emailはpropsの値が渡された場合のみ表示する
const Button = ({ name = 'ゲスト', email }: Props) => {
return (
<div>
<p>{name}さん</p>
{email && (
<p>email:{email}</p>
)}
</div>
)
}
戻り値の型付け
JSX要素を返せば型を指定していなくても推論してくれますが、明示しておけば正しい値が返されなかった場合エラーを表示してくれます。
const App = ({ name }: { name: string }): JSX.Element => <div>{name}</div>;
React.FC / VFC
React.FC / VFC については、TypeSctriptを導入するのであれば不要と考えられるのでここでは取り上げません。(今の私の業務では使っていないのですが、もしなにか誤りやアドバイスがあれば是非ご教授ください…!)
参考記事:【検証】React.FC と React.VFC はべつに使わなくていい説
useState 状態変数の型
型推論に任せる
useStateフックでは初期値を与えれば状態変数の型を推論してくれます。
なので極力初期値を与えて、推論させるようにします。
// プリミティブ値
const [name, setName] = useState("") // string型
const [count, setCount] = useState(0) // number型
const [isChecked, setIsCheked] = useState(false) // boolean型
// 配列
const [colors, setColors] = useState(["red", "blue"]) // string型の配列
const [numbers, setNumbers] = useState([1, 2, 3]) // number型の配列
ジェネリック型で指定する
- 基本の指定方法
useStateの状態変数に対して明示的に型を指定する場合は、ジェネリック型 <T>
を使います。
// useStateのジェネリック型<T>に明示的に型を指定する
const [name, setName] = useState<string>("") // string型
const [count, setCount] = useState<number>(0) // number型
const [isChecked, setIsChecked] = useState<boolean>(false) // boolean型
- 別途定義した型をジェネリック型で指定する
別途定義したオブジェクトを初期値にしたり、別途定義した型をジェネリック型で指定することもできます。
(型や初期値を複数のコンポーネントで使い回す場合や情報量が多くなる場合は、別ファイルに切り出しても良いかと思います。)
const initialUser = {
number: 1,
name: '名前',
email: 'test@mail.com'
}
type User = {
number: number
name: string
email: string
}
const [user, setUser] = useState<User>(initialUser)
- ユニオン型でnullを含める
値がnullの可能性があるときや、型は定めておきたいが初期値は後から決まるといった場合は、ユニオン型でnullを含めます。
// nullを含む場合はユニオン型を用いる
const [count, setCount] = useState<number | null>(null) // number型もしくはnull型
- 型アサーションを使う
状態変数の初期値が決まらないけれどnullは許可しないという場合には、型アサーションを使うことができます。
まず、以下の書き方ではエラーが出ます。
type User = {
number: number
name: string
email: string
}
const [user, setUser] = useState<User>({})
user[name] = 'ゲスト'
// userは「空のオブジェクト型」とTypeScriptは認識するのでコンパイルエラーになる
そこで有効なのが型アサーションです。
type User = {
number: number
name: string
email: string
}
const [user, setUser] = useState<User>({} as User)
// 型アサーションを使えばコンパイルエラーが出ない
ただし型アサーションはTypeScriptに型を偽っているだけなので、もし必要なプロパティを忘れていてもコンパイラエラーが指摘してくれないので、なるべく初期値を設定して型アサーションを使わずに済む実装にした方がベターです。
もし全てのプロパティが任意ならば、初期値に空オブジェクトを与えても問題ないので型アサーションは必要ありません。
type User = {
name?: string
email?: string
age?: number
}
const [user, setUser] = useState<User>({})
// userはUser型のうち任意のプロパティを持つ(空オブジェクトの可能性もある)
useEffect
useEffectの戻り値は undefined
もしくはクリーンアップ関数と決められており、戻り値を処理しないため型は必要ありません。
ただし関数や値を返してしまうとエラーになるので、アロー関数の書き方に注意が必要です。
以下の書き方では、アロー関数式=>
の本体に波括弧{}
なしに1行で書いた文が戻り値になってしまいます。
useEffect(() =>
setTimeout(() =>
// 処理
, 1000);
, []);
以下のように、本体を {}
で囲むようにしてください。
useEffect(() => {
setTimeout(() => {
// 処理
}, 1000);
}, []);
イベントオブジェクトの型
onClickやonChangeといったイベントハンドラで扱うイベントにも型があります。
イベントオブジェクトの型を知りたいときは、VSCodeがヒントをくれます。
例えばonClickなら、以下の状態でonClickの上にマウスをホバーさせると型情報を表示してくれます。
<button onClick={}></button>
以下のように要素のイベント属性に直接イベントハンドラを書く場合は、引数のイベントの型は推論されるので注釈が要りません。
<button onClick={(event) => //処理 }></button>
イベントハンドラの関数を別途定義する場合は、通常の関数と同様に型付けが必要になります。
onClickイベント
引数のイベントと戻り値に型付けをします。(戻り値は推論させてもOKです)
// Buttonクリックの場合
const onClickButton = ( event: React.MouseEvent<HTMLButtonElement, MouseEvent> ):void => {
// 処理
}
onChangeイベント
// Inputの場合
const onChangeInput = ( event: React.ChangeEvent<HTMLInputElement>):void => {
// 処理
}
複数種類の要素にイベントを適用する
上記の例を見ると、ジェネリック型の部分に HTMLButtonElement
HTMLInputElement
とあるように特定のHTML要素のための型であることがわかります。
もしDivタグ・Buttonタグ・Inputタグなどに同じイベントハンドラ関数を指定したい場合、通常は HTMLButtonElement | HTMLInputElement | 続く...
と使いたい要素分の型を記述する必要があります。
そんな時は、色んなHTML要素に共通して使える HTMLElement
型を使うこともできます。
type Props = {
onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void
}
以下の記事に詳しくまとめられており、非常に分かりやすかったです。
個人的には、精度を高めるためには面倒でも使う要素分だけ指定する方が良いのかな…?とも思いますが、 HTMLButtonElement
もHTMLInputElement
もHTMLElement
型を継承しているんだと知り、勉強になりました。
any型で諦めない React.EventCallback - Qiita
【番外編】Axios
Axiosを使って非同期通信を行う際に返されるレスポンスやエラーにも型があります。
import { AxiosError, AxiosResponse } from 'axios'
axios
.get('/url')
.then((response: AxiosResponse) => {
// 成功時の処理
})
.catch((error: AxiosError) => {
// エラー時の処理
})
おわりに
正直はじめは、何か書くたびにエラーが出るしコード量が増えて余計にわけわからんと思いました。
でも慣れてくると、型が明示してあると処理の流れを追わなくてもどういう値が入って来るのかパッと見でも分かりやすいし、エディタの補完が効くのは便利で間違いがあれば気付けてTypeScript良いなと思うようになりました。
今後もより実用的でスマートな型の活用法を学んでいきたいと思います。
参考記事