背景
この教材でReact x TypeScriptについてのセクションがあり、その中で学んだことをアウトプットする
型定義のメリットについて
1. プログラムを実行しなくても、欠陥に気づける
TypeScriptの型チェックは、プログラムを実行する前に行われる。具体的には2つのタイミングがある
- エディタ上(リアルタイム): VSCodeなどのエディタが裏でLanguage Server(ts-server)を動かし、コードを書いている最中にリアルタイムで型の不整合を検出して赤波線を出してくれる
-
コンパイル時(
tsc実行時): TypeScriptコンパイラがコード全体を型チェックし、JSに変換する
どちらも実行前に型の不整合を検出する仕組みという点が重要。JavaScriptだと、例えばcompletedにbooleanを期待しているのにstringを渡しても、実際に動かすまで気づけない
2. プログラムの可読性が上がる
型定義があることで、「この関数は何を受け取って、何を返すのか」がコードを読むだけで分かる
// 型なし: propsに何が入ってくるか、コードを追わないと分からない
export const Todo = (props) => {
// 型あり: idを受け取ることが一目瞭然
export const Todo = (props: Pick<TodoType, 'id'>) => {
3. エディターの補完機能が使える
型を定義しておくと、エディタが「このオブジェクトにはこのプロパティがあるよ」と候補を出してくれる。プロパティ名を正確に覚えていなくても、.を打つだけで候補が表示されるので、タイプミスも減る。
Reactでどうやって型定義するのか
前提: アプリについて
ボタンを押すとJSON Placeholderというサービスから、axiosを用いてTODOデータを取得し、表示するという簡単なアプリ
// App.tsx
import axios from 'axios';
import { useState } from 'react';
import { Todo } from './Todo';
import { UserProfile } from './UserProfile';
import { Text } from './Text';
import type { TodoType } from './types/todo';
import type { User } from './types/user';
const user: User = {
name: 'taji',
hobbies: ['映画', 'サッカー'],
};
function App() {
const [todos, setTodos] = useState<Array<TodoType>>([]);
const onClickFetchData = () => {
axios
.get<Array<TodoType>>('https://jsonplaceholder.typicode.com/todos')
.then((res) => {
console.log(res.data);
setTodos(res.data);
});
};
return (
<>
<UserProfile user={user} />
<button onClick={onClickFetchData}>データを取得する</button>
{todos.map((todo) => (
<Todo
key={todo.userId}
title={todo.title}
userId={todo.userId}
completed={todo.completed}
/>
))}
</>
);
}
export default App;
// Todo.tsx
import type { TodoType } from './types/todo';
export const Todo = (props: Omit<TodoType, 'id'>) => {
const { title, userId, completed = false } = props;
const complete = completed ? '[完]' : '[未]';
return <p>{`${complete}${title}(ユーザー: ${userId})`}</p>;
};
// UserProfile.tsx
import type { User } from './types/user';
type Props = {
user: User;
};
export const UserProfile = (props: Props) => {
const { user } = props;
return (
<dl>
<dt>名前</dt>
<dd>{user.name}</dd>
<dt>趣味</dt>
<dd>{user.hobbies?.join('/')}</dd>
</dl>
);
};
//todo.ts 型定義ファイル
export type TodoType = {
userId: number;
id: number;
title: string;
completed: boolean;
};
//user.ts 型定義ファイル
export type User = {
name: string;
hobbies?: Array<string>;
};
型の定義方法
Reactで型定義する主なパターンを紹介
1. 型定義ファイルを作成する
types/ディレクトリに型定義ファイルを配置し、typeで型を定義する
// types/todo.ts
export type TodoType = {
userId: number;
id: number;
title: string;
completed: boolean;
};
// types/user.ts
export type User = {
name: string;
hobbies?: Array<string>; // ?を付けると省略可能(渡しても渡さなくてもOK)
};
-
?を付けたプロパティはオプショナルになる。つまり、そのプロパティは渡しても渡さなくてもエラーにならない - 型定義ファイルを分離することで、複数のコンポーネントから同じ型を再利用できる
2. useStateにジェネリクスで型を指定する
const [todos, setTodos] = useState<Array<TodoType>>([]);
-
todosの中身はTodoTypeの配列しか入らない -
setTodosはTodoType[]、もしくは(prev: TodoType[]) => TodoType[]というコールバック関数を受け付ける - 初期値は空配列
[]
3. axiosのレスポンスに型を指定する
axios.get<Array<TodoType>>('https://jsonplaceholder.typicode.com/todos')
-
res.dataの型がTodoTypeの配列であると宣言している(get自体の戻り値はPromise<AxiosResponse<T>>であり、ジェネリクスTがres.dataの型に反映される) -
res.dataにアクセスする際、型の補完が効くようになる(res.data[0].と打てばプロパティ候補が出る) - 型定義にないプロパティを使おうとすると、エディタ上でエラーが表示される
- あくまで「
res.dataをこの型として扱う」という宣言であり、実行時にAPIのレスポンスが型と一致するかどうかは検証されない
APIのレスポンスは以下のような形式で返ってくる
[
{"userId": 1, "id": 1, "title": "タイトル1", "completed": false},
{"userId": 2, "id": 2, "title": "タイトル2", "completed": true},
{"userId": 3, "id": 3, "title": "タイトル3", "completed": false}
]
4. コンポーネントのpropsに型を指定する
propsに型を指定することで、親コンポーネントから渡されるデータを制限できる。
export const Todo = (props: Omit<TodoType, 'id'>) => {
-
Omit<TodoType, 'id'>:TodoTypeからidを除外した型をpropsとして受け取る - 指定した型以外のpropsを渡すとエディタ上でエラーになる
- 必須のpropsが渡されていない場合もエラーになる
ユーティリティ型: OmitとPick
TypeScriptには既存の型を加工するためのユーティリティ型が用意されている。
| ユーティリティ型 | 意味 | 例 |
|---|---|---|
Omit<T, Keys> |
型TからKeysを除外した型を返す |
Omit<TodoType, 'id'> → id以外 |
Pick<T, Keys> |
型TからKeysだけを抽出した型を返す |
Pick<TodoType, 'title'> → titleのみ |
-
Omit:「これは要らない」と除外するイメージ -
Pick:「これだけ欲しい」と選び取るイメージ
5. FC(FunctionComponent)を使った型定義
import type { FC } from 'react';
type Props = {
color: string;
fontSize: string;
};
export const Text: FC<Props> = (props) => {
const { color, fontSize } = props;
return <p style={{ color, fontSize }}>テキスト</p>;
};
-
FC<Props>はReactが提供する関数コンポーネント用の型 - コンポーネントの戻り値の型が
ReactNodeとして保証される(ReactNodeはReactElementだけでなくstringやnumber、nullなども含む広い型)
詰まったところ
verbatimModuleSyntax
型をimportする際、以下のようなエラーが出た。
'TodoType' is a type and must be imported using a type-only import
when 'verbatimModuleSyntax' is enabled.
'FC' is a type and must be imported using a type-only import
when 'verbatimModuleSyntax' is enabled.
原因: tsconfig.app.jsonでverbatimModuleSyntax: trueが設定されていたため。
{
"compilerOptions": {
"verbatimModuleSyntax": true,
// ...
}
}
そもそもverbatimModuleSyntaxとは?
TypeScriptのコードは最終的にJavaScriptに変換(コンパイル)される。その際、型は全部消える。型はあくまで開発時に便利な機能であって、実行時には不要だから
この設定をONにすると、「型だけのimportには必ずtypeを付けろ」と強制される
// NG: typeキーワードがない
import { TodoType } from './types/todo';
// OK: typeキーワードを付ける
import type { TodoType } from './types/todo';
なぜtypeを明示するのか?
- コンパイル時に「これは型だから消していい」と明確になる
- コードを読む人にとって「これは値ではなく型のimportだ」と一目で分かる
- バンドラーがより効率的にツリーシェイキング(不要コードの除去)できる
疑問点
引数に型を指定する方法 vs FCを使う方法は何が違うのか?
以下の2つの書き方は、結果としてほぼ同じ動きをする。
方法1: 引数に直接型を指定
export const Todo = (props: Omit<TodoType, 'id'>) => {
方法2: FCを使う
export const Todo: FC<Omit<TodoType, 'id'>> = (props) => {
違いまでは、調査できなかたのでまた別の機会に
感想
- だいぶ前にPHP触っていた時はめんどいなぁと思っていた型定義だが、ユーザー体験はかなり良さそうと思った
参考



