1. はじめに
今回はTypeScriptの基礎から、React × TypeScriptの実践的な使い方まで幅広く学びました。
変数・引数・返却値への型指定に始まり、APIデータの型定義、propsの型定義、型定義ファイルの管理、オプショナルチェーンまでを順番に整理していきます。
2. TypeScriptとは
TypeScriptはJavaScriptに型の仕組みを追加した言語のようです。
ブラウザやNode.jsはTypeScriptをそのまま実行できないため、最終的にはJavaScriptへコンパイルして使うみたいです。
型を書くことでエディタが間違いをリアルタイムに教えてくれるため、バグを事前に防ぎやすくなると理解しました。
3. 基本的な型と型指定
TypeScriptでは変数・引数・返却値それぞれに型を指定できるみたいです。
今回学んだ基本的な型の書き方を順に整理していきます。
3.1 変数の型指定
今回確認した変数の型アノテーション(: 型名 の形で型を明示する記法)のコードです。
/* eslint-disable */
// boolean型:trueかfalseのみ代入できる
let bool: boolean = true;
// 配列型:ジェネリクス形式(<>の中に型を渡す書き方)
let arr1: Array<number> = [1, 2, 3];
// 配列型:ショートハンド形式
let arr2: number[] = [1, 2, 3];
変数名の後ろに : 型名 の形で型を書くみたいです。
boolean は true か false のみ代入できる型で、number は数値を表す型のようです。
配列の型指定には Array<number> と number[] の2通りがあり、どちらも同じ意味と理解しました。
「型アノテーション」とは : 型名 の形で型を明示する記法のことです。
「ジェネリクス」とは <型名> の形で型を外から渡す仕組みのことです。
3.2 引数の型指定
関数の引数に型を指定するコードです。
// 引数に型を指定した関数
const greet = (name: string): void => {
console.log(`こんにちは、${name}さん`);
};
greet("橋本"); // → 出力: こんにちは、橋本さん
引数の後ろに : 型名 と書くことで、その引数に渡せる値の種類を制限できるみたいです。
たとえば name: string と指定すると、数値や真偽値を渡そうとしたときにエラーになるようです。
3.3 返却値の型指定
関数の返却値に型を指定するコードです。
// 返却値にnumber型を指定した関数
const add = (a: number, b: number): number => {
// 計算結果を返す
return a + b;
};
// 返却値がない場合はvoidを指定
const funcA = (): void => {
const test = "test";
};
() の後ろに : 型名 と書くことで返却値の型を指定できるみたいです。
何も返さない関数には void を指定するようです。
void を指定しておくと、意図しない return 文が混入したときにエラーで気づけるみたいです。
4. React × TypeScriptの準備
ReactプロジェクトにTypeScriptを導入するには、Viteのテンプレートを使うのが手軽なようです。
Viteを使ってTypeScript対応のReactプロジェクトを作成するコマンドです。
# TypeScript対応のReactプロジェクトを作成
npm create vite@latest my-app -- --template react-ts
作成されたプロジェクトでは、コンポーネントファイルの拡張子が .tsx、通常のTypeScriptファイルは .ts になっているみたいです。
.tsx はJSXを含むTypeScriptファイル専用の拡張子のようです。
.ts は通常の TypeScript ファイル、.tsx は JSX 構文を含む TypeScript ファイルに使います。
コンポーネントファイルは .tsx、型定義のみのファイルは .ts にするのが一般的です。
5. 型がないせいでバグっているアプリの例
型定義を行わない状態では、データ取得自体はできても、取得したデータを使うときに危険な状態になるみたいです。
型がない場合に何が起きるかを確認しました。
型指定なしでaxiosを使っている状態のコードです。
// 型がない状態でAPIデータをそのまま使う例
const onClickFetchData = () => {
axios.get("https://jsonplaceholder.typicode.com/todos").then((res) => {
// res.dataの型がanyになり、どんな操作もエラーにならない
setTodos(res.data);
});
};
axios.get() にジェネリクスで型を指定しないと、res.data の型が any になるみたいです。
問題はデータが取れるかどうかではなく、取れたデータを安全に使えるかどうかのようです。
any 型の状態だと、存在しないプロパティへのアクセスやタイポをしても書いた時点では気づけず、実行して初めてバグに気づくことになるみたいです。
// any型の場合、これらが全てエラーにならない
res.data[0].tittle // タイポ → undefinedが返るだけ
res.data[0].done // 存在しないプロパティ → undefinedが返るだけ
any 型はすべての型チェックを無効化してしまいます。
TypeScript を使う意味が薄れるため、意図せず any になっていないか注意が必要みたいです。
6. 取得データの型を定義しバグを防ぐ
axiosと useState に型を指定することで、APIレスポンスの型を安全に扱えるみたいです。
今回はまず型定義ファイルを用意し、それをaxiosと useState に適用しました。
TodoデータのShape(形)を定義するファイルです。
// types/todo.ts
// Todoデータの型定義
export type TodoType = {
userId: number;
id: number;
title: string;
completed: boolean;
};
定義した型をaxiosと useState に適用したコードです。
import axios from "axios";
import { useState } from "react";
import type { TodoType } from "./types/todo";
function App() {
// 初期値が空配列のときは型を明示しないとnever[]に推論されてしまう
const [todos, setTodos] = useState<Array<TodoType>>([]);
const onClickFetchData = () => {
// ジェネリクスでレスポンスの型を指定
axios
.get<Array<TodoType>>("https://jsonplaceholder.typicode.com/todos")
.then((res) => {
// res.dataがArray<TodoType>型として推論される
setTodos(res.data);
});
};
}
axios.get<Array<TodoType>>() のようにジェネリクスで型を渡すと、res.data が Array<TodoType> 型として推論されるみたいです。
これにより存在しないプロパティへのアクセスがエラーとして検出されるようになると理解しました。
useState([]) のように型指定なしで空配列を渡すと、TypeScript が「何も入れられない配列」を意味する never[] と推論してしまいます。
初期値が空の場合は必ず useState<Array<TodoType>>([]) のように型を明示することが大切です。
7. propsに型を定義しよう
コンポーネントが受け取るpropsに型を定義することで、渡し忘れや型の不一致をエラーとして検出できるみたいです。
まず FC の基本的な使い方をシンプルな例で確認してから、Omit を組み合わせた実践的な使い方へと進めていきます。
7.1 FCを使った基本的なprops型定義
Textコンポーネントの全体コードです。
import type { FC } from "react";
// Propsの型を定義
type Props = {
color: string;
fontSize: string;
};
// FC<Props>で「このコンポーネントはPropsを受け取る」と指定する
export const Text: FC<Props> = (props) => {
const { color, fontSize } = props;
return <p style={{ color, fontSize }}>テキストです</p>;
};
FC は「このコンポーネントはどんなpropsを受け取るか」を指定するための型のようです。
FC は FunctionComponent の略で、Reactが提供している型みたいです。
FC<Props> と書くことでpropsの型指定と同時に、戻り値が JSX.Element であることも自動的に保証されるため、返却値の型を別途指定しなくて済むのが便利だと感じました。
7.2 Omitを使った応用的なprops型定義
FCの使い方を押さえたところで、次は既存の型定義を活用する Omit を組み合わせた書き方を確認しました。
Todoコンポーネントの全体コードです。
// import type → 型情報だけをインポート(コンパイル後のJSには残らない)
import type { FC } from "react";
import type { TodoType } from "./types/todo";
// Omit<TodoType, "id"> = TodoTypeからidを除いた型をPropsとして使う
export const Todo: FC<Omit<TodoType, "id">> = (props) => {
const { title, userId, completed } = props;
// completedの値に応じてラベルを切り替える
const completeMark = completed ? "[完了]" : "[未完了]";
return <p>{`${completeMark}${title}(ユーザー: ${userId})`}</p>;
};
Omitについて
Omit<TodoType, "id"> は「TodoType から id だけ取り除いた型を作る」という意味のようです。
TodoコンポーネントはAPIから取得した id を表示に使わないため、propsとして受け取る必要がありません。
わざわざ新しい型を1から書き直さなくても、既存の型から不要な部分だけ除けるのが便利だと感じました。
// TodoTypeはこういう型
type TodoType = {
userId: number;
id: number; // ← これだけ除きたい
title: string;
completed: boolean;
};
// Omit<TodoType, "id"> はこういう型と同じ意味になる
type TodoTypeWithoutId = {
userId: number;
title: string;
completed: boolean;
};
import typeについて
import type は型情報だけをインポートする構文のようです。
通常の import との違いは、JavaScriptにコンパイルされるときに完全に消える点みたいです。
つまり「TypeScriptのチェックにだけ使って、実際の実行ファイルには含めない」という書き方のようです。
// import type → コンパイル後のJSには残らない(型チェック専用)
import type { FC } from "react";
import type { TodoType } from "./types/todo";
// 通常のimport → コンパイル後のJSにも残る(実行時に実際に使う)
import { useState } from "react";
import axios from "axios";
Omit<T, K> の他にも、特定のプロパティだけを取り出す Pick<T, K> や、すべてのプロパティを省略可能にする Partial<T> などのユーティリティ型があります。
8. 型定義を効率的に管理しよう
型定義をコンポーネントに直接書くと管理しづらくなるため、types/ ディレクトリに分けて管理するパターンが便利みたいです。
今回は types/todo.ts と types/user.ts の2ファイルに型定義を切り出しました。
今回のファイル構成です。
src/
├── types/
│ ├── todo.ts
│ └── user.ts
├── App.tsx
├── Todo.tsx
├── UserProfile.tsx
└── Text.tsx
User型を定義したファイルのコードです。
// types/user.ts
export type User = {
name: string;
// ?をつけると省略可能なプロパティになる
hobbies?: Array<string>;
};
各コンポーネントでは import type を使って型定義ファイルから読み込むみたいです。
型定義を共通ファイルにまとめることで、複数のコンポーネントで同じ型を再利用できるのが便利だと感じました。
9. オプショナルチェーンでnull安全なコードを書く
オプショナルプロパティ(? 付きのプロパティ)にアクセスするときは ?.(オプショナルチェーン)を使うことで、undefined のときのエラーを防げるみたいです。
今回はUserProfileコンポーネントでその実践的な使い方を確認しました。
UserProfileコンポーネントの全体コードです。
import type { FC } from "react";
import type { User } from "./types/user";
type Props = {
user: User;
};
export const UserProfile: FC<Props> = (props) => {
const { user } = props;
return (
<dl>
<dt>名前</dt>
<dd>{user.name}</dd>
<dt>趣味</dt>
{/* hobbiesはオプショナルなので?.でアクセスする */}
<dd>{user.hobbies?.join(" / ")}</dd>
</dl>
);
};
user.hobbies?.join(" / ") と書くことで、hobbies が undefined のときは処理をスキップして undefined を返し、存在するときだけ join() を実行してくれるみたいです。
実際にApp.tsxでUserを定義する際、hobbies を省略してもTypeScriptがエラーを出さないことを確認しました。
hobbiesを省略したUserオブジェクトの定義コードです。
// App.tsx(抜粋)
// hobbiesを省略してもエラーにならない(オプショナルなため)
const user: User = {
name: "橋本",
// hobbies: ["映画", "ゲーム"],
};
?. なしでオプショナルプロパティにアクセスすると TypeScript がエラーを出します。
user.hobbies.join(...) のように書くと「hobbies は undefined の可能性がある」というエラーになるため、必ず ?. とセットで使うことが大切です。
まとめ
今回はTypeScriptの基本型指定からReact × TypeScriptの実践的な型定義まで幅広く学びました。
型をつけることでバグを未然に防ぎながら安心して書けるコードが増えていく感覚が掴めました。
今回の気づき
型定義ファイルを types/ ディレクトリに分離するパターンを学んで、型の再利用性と管理のしやすさが大きく向上することに気づきました。
FC と Omit を組み合わせることで、既存の型を活かしながらコンポーネントのpropsを柔軟に定義できるのが面白いと感じます。
useState や axios.get にジェネリクスで型を渡す書き方は今後も頻繁に登場しそうなので、しっかり身につけておきたいと思いました。
ハマりやすいポイント
-
useState([])のように型指定なしで空配列を渡すとnever[]に推論されてしまい、後から要素を追加しようとするとエラーになるみたいです。 - オプショナルプロパティに
?.なしでアクセスすると型エラーになるため、?付きのプロパティには必ず?.とセットで使うのが大切なようです。 -
import typeは値としては使えないため、実行時に実際に使う関数やコンポーネントは通常のimportで読み込む必要があるみたいです。