これまでの記事でTypeScriptのコード例も紹介してきましたが、この記事ではTypeScriptとReactの組み合わせについてより深く学びます。
高度な型定義、ジェネリクス、ユーティリティ型、型ガードなど、実践的なTypeScriptの使い方を詳しく解説します。
Contents
基本的な型定義の復習
コンポーネントのProps・イベントハンドラへの型付けの基本を習得します。
「なんとなく動く」コードから「型エラーをコンパイル時に検出できる」堅牢なコードへの第一歩です。
コンポーネントのProps
PropsにはTypeScriptのinterfaceを使って型を定義します。
?をつけたプロパティは省略可能(オプショナル)になり、定義されていないプロパティを渡すとコンパイルエラーになるため、使い方の誤りを早期に発見できます。
// 基本的な型定義
interface ButtonProps {
text: string
onClick: () => void
disabled?: boolean
}
function Button({ text, onClick, disabled = false }: ButtonProps): JSX.Element {
return (
<button onClick={onClick} disabled={disabled}>
{text}
</button>
)
}
// childrenを含む場合
interface CardProps {
title: string
children: React.ReactNode
}
function Card({ title, children }: CardProps): JSX.Element {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
)
}
イベントハンドラの型
Reactのイベントには、対応するHTML要素ごとに専用の型が用意されています。
たとえばボタンのクリックにはReact.MouseEvent<HTMLButtonElement>、インプットの変更にはReact.ChangeEvent<HTMLInputElement>を使います。
正しい型を指定することで、e.target.valueなどのプロパティへの安全なアクセスが保証されます。
// クリックイベント
const handleClick = (e: React.MouseEvent<HTMLButtonElement>): void => {
console.log('クリックされた')
}
// 入力イベント
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
console.log(e.target.value)
}
// フォーム送信
const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault()
console.log('送信')
}
// キーボードイベント
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
console.log('Enter押下')
}
}
ジェネリクスの活用
型を「引数」として受け取るジェネリクスを使い、型安全性を保ちながら再利用できる汎用コンポーネントやカスタムフックの設計パターンを習得します。
汎用的なリストコンポーネント
<T>という型パラメータを使うことで、ユーザーリストでも商品リストでも同じコンポーネントが使い回せます。
型パラメータにより、renderItemに渡されるアイテムの型が自動的に推論されるため、補完と型チェックの恩恵を受けられます。
interface ListProps<T> {
items: T[]
renderItem: (item: T) => React.ReactNode
keyExtractor: (item: T) => string | number
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>): JSX.Element {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>
{renderItem(item)}
</li>
))}
</ul>
)
}
// 使用例
interface User {
id: number
name: string
email: string
}
function UserList() {
const users: User[] = [
{ id: 1, name: '太郎', email: 'taro@example.com' },
{ id: 2, name: '花子', email: 'hanako@example.com' }
]
return (
<List
items={users}
renderItem={(user) => (
<div>
<strong>{user.name}</strong>
<span>{user.email}</span>
</div>
)}
keyExtractor={(user) => user.id}
/>
)
}
汎用的なモーダルコンポーネント
モーダルに表示するデータの型をジェネリクスで受け取ることで、ユーザー情報・商品詳細・確認ダイアログなど、あらゆる用途に使い回せる汎用モーダルが作れます。
renderContentに渡すデータの型がジェネリクスで縛られているため、存在しないプロパティへのアクセスはコンパイルエラーになります。
interface ModalProps<T> {
isOpen: boolean
onClose: () => void
data: T
renderContent: (data: T) => React.ReactNode
}
function Modal<T>({
isOpen,
onClose,
data,
renderContent
}: ModalProps<T>): JSX.Element | null {
if (!isOpen) return null
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{renderContent(data)}
<button onClick={onClose}>閉じる</button>
</div>
</div>
)
}
// 使用例
interface Product {
id: number
name: string
price: number
description: string
}
function ProductModal() {
const [isOpen, setIsOpen] = useState(false)
const product: Product = {
id: 1,
name: 'ノートPC',
price: 120000,
description: '高性能なノートパソコン'
}
return (
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
data={product}
renderContent={(product) => (
<div>
<h2>{product.name}</h2>
<p>¥{product.price.toLocaleString()}</p>
<p>{product.description}</p>
</div>
)}
/>
)
}
汎用的なカスタムフック
データ取得ロジックをジェネリクスのカスタムフックに切り出すことで、どんなAPIレスポンスの型にも対応できるuseFetchが作れます。
呼び出し時にuseFetch<User>(url)と型を指定するだけで、dataの型が自動的にUser | nullと推論されます。
interface UseFetchResult<T> {
data: T | null
loading: boolean
error: string | null
refetch: () => void
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)
const fetchData = useCallback(async () => {
try {
setLoading(true)
const response = await fetch(url)
if (!response.ok) throw new Error('Fetch failed')
const json: T = await response.json()
setData(json)
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : '不明なエラー')
setData(null)
} finally {
setLoading(false)
}
}, [url])
useEffect(() => {
fetchData()
}, [fetchData])
return { data, loading, error, refetch: fetchData }
}
// 使用例
interface User {
id: number
name: string
email: string
}
function UserProfile() {
const { data, loading, error } = useFetch<User>('https://api.example.com/user/1')
if (loading) return <p>読み込み中...</p>
if (error) return <p>エラー: {error}</p>
if (!data) return <p>データがありません</p>
return (
<div>
<h2>{data.name}</h2>
<p>{data.email}</p>
</div>
)
}
ユーティリティ型
TypeScript組み込みのユーティリティ型(Partial・Required・Pick・Omit・Record)を使い、既存の型を変換・派生させる手法を習得します。
型の重複定義を減らし、変更に強いコードが書けるようになります。
Partial – すべてのプロパティを省略可能に
Partial<T>はすべてのプロパティをオプショナルにした型を生成します。
更新APIの引数のように「変更したいフィールドだけを渡せる」パターンに便利です。
interface User {
id: number
name: string
email: string
age: number
}
// すべてのプロパティが省略可能
function updateUser(id: number, updates: Partial<User>): void {
// updates は { name?: string, email?: string, age?: number } のような型
console.log('更新:', updates)
}
// 使用例
updateUser(1, { name: '新しい名前' }) // OK
updateUser(1, { email: 'new@example.com', age: 30 }) // OK
Required – すべてのプロパティを必須に
Required<T>は逆にすべてのプロパティを必須にします。
オプショナルで定義した設定オブジェクトを「すべて揃っていること」を前提に処理する場面で役立ちます。
interface Config {
apiUrl?: string
timeout?: number
retries?: number
}
// すべてのプロパティが必須
function validateConfig(config: Required<Config>): boolean {
// config は { apiUrl: string, timeout: number, retries: number }
return config.apiUrl.length > 0 && config.timeout > 0
}
Pick – 特定のプロパティだけを選択
Pick<T, K>は指定したプロパティだけを持つ型を作ります。
一覧表示用など、全フィールドが不要なコンポーネントに渡す型を絞り込めます。
interface User {
id: number
name: string
email: string
password: string
role: string
}
// nameとemailだけを選択
type UserPreview = Pick<User, 'name' | 'email'>
function displayUser(user: UserPreview): JSX.Element {
return (
<div>
<p>{user.name}</p>
<p>{user.email}</p>
</div>
)
}
Omit – 特定のプロパティを除外
Omit<T, K>は指定したプロパティを除いた型を作ります。
パスワードのような機密フィールドをUIに渡さないよう型レベルで強制できます。
interface User {
id: number
name: string
email: string
password: string
}
// passwordを除外
type SafeUser = Omit<User, 'password'>
function displayUser(user: SafeUser): JSX.Element {
return (
<div>
<p>ID: {user.id}</p>
<p>名前: {user.name}</p>
<p>メール: {user.email}</p>
</div>
)
}
Record – キーと値の型を指定
Record<K, V>はキーの型と値の型を明示したオブジェクト型を作ります。
定義済みのキー集合(Union型)に対して漏れなく値を定義することを強制できます。
type Role = 'admin' | 'user' | 'guest'
const permissions: Record<Role, string[]> = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read']
}
// 動的なオブジェクト
type UserStatus = Record<number, string>
const userStatuses: UserStatus = {
1: 'active',
2: 'inactive',
3: 'pending'
}
Union型とリテラル型
決まった文字列のどれか」を表すリテラル型と、複数の型の「いずれか」を表すUnion型を組み合わせて、取りうる値を型で表現する手法を習得します。
無効な値の混入を防ぎ、switch文と組み合わせた網羅的な分岐処理が書けるようになります。
リテラル型
文字列リテラルのUnion型を使うと、'primary'・'secondary'・'danger'以外の文字列を渡した時点でコンパイルエラーになります。
文字列の打ち間違いによるバグをゼロにできます。
type ButtonVariant = 'primary' | 'secondary' | 'danger'
type Size = 'small' | 'medium' | 'large'
interface ButtonProps {
variant: ButtonVariant
size: Size
children: React.ReactNode
}
function Button({ variant, size, children }: ButtonProps): JSX.Element {
const className = `btn btn-${variant} btn-${size}`
return <button className={className}>{children}</button>
}
// 使用例
<Button variant="primary" size="large">クリック</Button>
// variant="invalid" はエラーになる
判別可能なUnion型
statusフィールドを共通のリテラル型として持つ複数のinterfaceをUnion型にすると、switch (state.status)で型が自動的に絞り込まれます(Discriminated Union)。
ローディング・成功・エラーの3状態を漏れなく扱えるパターンとして非常に有用です。
interface LoadingState {
status: 'loading'
}
interface SuccessState<T> {
status: 'success'
data: T
}
interface ErrorState {
status: 'error'
error: string
}
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState
function DataDisplay<T>({ state }: { state: AsyncState<T> }): JSX.Element {
// statusで型を判別
switch (state.status) {
case 'loading':
return <p>読み込み中...</p>
case 'success':
return <div>データ: {JSON.stringify(state.data)}</div>
case 'error':
return <p>エラー: {state.error}</p>
}
}
複雑なUnion型の例
入力タイプごとに異なるプロパティを持つinterfaceをUnion型でまとめることで、type: 'number'の時だけmin・maxが存在するといった「型の形がtypeによって変わる」コンポーネントを型安全に実装できます。
interface TextInput {
type: 'text'
value: string
onChange: (value: string) => void
}
interface NumberInput {
type: 'number'
value: number
min?: number
max?: number
onChange: (value: number) => void
}
interface CheckboxInput {
type: 'checkbox'
checked: boolean
onChange: (checked: boolean) => void
}
type InputProps = TextInput | NumberInput | CheckboxInput
function Input(props: InputProps): JSX.Element {
switch (props.type) {
case 'text':
return (
<input
type="text"
value={props.value}
onChange={(e) => props.onChange(e.target.value)}
/>
)
case 'number':
return (
<input
type="number"
value={props.value}
min={props.min}
max={props.max}
onChange={(e) => props.onChange(Number(e.target.value))}
/>
)
case 'checkbox':
return (
<input
type="checkbox"
checked={props.checked}
onChange={(e) => props.onChange(e.target.checked)}
/>
)
}
}
型ガード
Union型など「複数の型の可能性がある値」を安全に絞り込む型ガードの書き方を習得します。
typeof・in演算子・カスタム型ガード関数の使い分けができるようになります。
typeof型ガード
typeofでプリミティブ型を判定すると、if分岐の中でTypeScriptが型を自動的に絞り込みます。
string側ではtoUpperCase()、number側ではtoFixed()が使えることをコンパイラが把握してくれます。
function formatValue(value: string | number): string {
if (typeof value === 'string') {
// この中ではvalueはstring型
return value.toUpperCase()
} else {
// この中ではvalueはnumber型
return value.toFixed(2)
}
}
in型ガード
'プロパティ名' in オブジェクトで特定のプロパティの存在を確認すると、Union型のどちらかに絞り込めます。
判別子フィールドを持たない型の分岐に便利です。
interface Cat {
type: 'cat'
meow: () => void
}
interface Dog {
type: 'dog'
bark: () => void
}
type Animal = Cat | Dog
function makeSound(animal: Animal): void {
if ('meow' in animal) {
// animalはCat型
animal.meow()
} else {
// animalはDog型
animal.bark()
}
}
カスタム型ガード
obj is Userという返り値の型を持つ関数を定義することで、APIレスポンスなどunknown型の値が期待する構造かどうかを実行時に検証しつつ、TypeScriptに型を伝えられます。
型アサーション(as)と異なり、実際の値の構造を確認するため安全です。
interface User {
id: number
name: string
email: string
}
// 型ガード関数
function isUser(obj: any): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
typeof obj.id === 'number' &&
typeof obj.name === 'string' &&
typeof obj.email === 'string'
)
}
function processData(data: unknown): void {
if (isUser(data)) {
// この中ではdataはUser型
console.log(data.name)
console.log(data.email)
} else {
console.log('Invalid user data')
}
}
高度なコンポーネント型定義
関数コンポーネントの推奨される型定義の書き方、forwardRefの型付け、HTML標準属性を継承したPropsの拡張パターンを習得します。
ライブラリ品質の再利用可能なコンポーネントを型安全に実装できるようになります。
関数コンポーネントの型
現在の推奨は「戻り値の型注釈を省略し、TypeScriptの型推論に任せる」方法3です。
React.FC(方法2)はかつてよく使われましたが、不要なchildrenの暗黙的な型付けやdisplayNameの設定など副作用があるため、最近は使われなくなっています。
// 方法1: 明示的な戻り値
function Component1(props: { name: string }): JSX.Element {
return <div>{props.name}</div>
}
// 方法2: React.FC(最近は非推奨)
const Component2: React.FC<{ name: string }> = ({ name }) => {
return <div>{name}</div>
}
// 方法3: 推奨される書き方
function Component3({ name }: { name: string }) {
return <div>{name}</div>
}
forwardRefの型定義
forwardRefには<DOM要素の型, Propsの型>の2つの型パラメータを指定します。
これにより、親コンポーネントでuseRef<HTMLInputElement>と合わせることで、inputRef.currentがHTMLInputElementとして型推論され、focus()などのメソッドに安全にアクセスできます。
interface InputProps {
placeholder?: string
value: string
onChange: (value: string) => void
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ placeholder, value, onChange }, ref) => {
return (
<input
ref={ref}
type="text"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
)
}
)
// 使用例
function Parent() {
const inputRef = useRef<HTMLInputElement>(null)
const [value, setValue] = useState('')
const focusInput = () => {
inputRef.current?.focus()
}
return (
<div>
<Input ref={inputRef} value={value} onChange={setValue} />
<button onClick={focusInput}>フォーカス</button>
</div>
)
}
コンポーネントpropsの拡張
extends React.ButtonHTMLAttributes<HTMLButtonElement>とすることで、onClick・disabled・typeなどHTMLボタンが持つすべての標準属性を独自のProps型に継承できます。
...propsでまとめてボタンに渡せるため、スプレッド構文で属性を転送するラッパーコンポーネントに最適です。
// HTMLの標準属性を継承
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary'
}
function Button({ variant = 'primary', children, ...props }: ButtonProps): JSX.Element {
return (
<button className={`btn btn-${variant}`} {...props}>
{children}
</button>
)
}
// 使用例
<Button onClick={() => console.log('clicked')} disabled>
クリック
</Button>
Context APIの型定義
createContext・Provider・カスタムフックをすべて型安全に実装するパターンを習得します。
useContextの戻り値がundefinedになりうる問題をカスタムフックで解消し、コンテキストの不適切な使用をコンパイルエラーとして検出できるようになります。
Contextの型をcreateContext<AuthContextType | undefined>(undefined)で初期化し、専用のカスタムフック(useAuth)内でundefinedチェックを行うのが定番パターンです。
Provider外でuseAuthを使うと実行時エラーが投げられるため、誤用を確実に検知できます。
interface User {
id: number
name: string
email: string
}
interface AuthContextType {
user: User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
isAuthenticated: boolean
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
interface AuthProviderProps {
children: React.ReactNode
}
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null)
const login = async (email: string, password: string): Promise<void> => {
// ログイン処理
const userData: User = { id: 1, name: '太郎', email }
setUser(userData)
}
const logout = (): void => {
setUser(null)
}
const value: AuthContextType = {
user,
login,
logout,
isAuthenticated: !!user
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
export function useAuth(): AuthContextType {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuthはAuthProvider内で使用してください')
}
return context
}
実践:型安全なフォーム
ジェネリクス・ユーティリティ型・Mapped Typesを組み合わせた実践的なフォーム管理カスタムフックの実装パターンを習得します。
バリデーションエラーの型をフォームの型から自動導出することで、フィールドの追加・削除に追従する型安全なフォームが作れるようになります。
FormErrors<T>は[K in keyof T]?: stringというMapped Typeで定義されており、フォームの型Tと同じキーを持ちながら値がオプショナルな文字列(エラーメッセージ)になります。
これにより、存在しないフィールド名のエラーを設定しようとするとコンパイルエラーになります。
// フォームの値の型
interface LoginForm {
email: string
password: string
rememberMe: boolean
}
// バリデーションエラーの型
type FormErrors<T> = {
[K in keyof T]?: string
}
// カスタムフック
function useForm<T extends Record<string, any>>(
initialValues: T,
validate: (values: T) => FormErrors<T>
) {
const [values, setValues] = useState<T>(initialValues)
const [errors, setErrors] = useState<FormErrors<T>>({})
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({})
const handleChange = (name: keyof T, value: any): void => {
setValues(prev => ({ ...prev, [name]: value }))
}
const handleBlur = (name: keyof T): void => {
setTouched(prev => ({ ...prev, [name]: true }))
const validationErrors = validate(values)
setErrors(validationErrors)
}
const handleSubmit = (
onSubmit: (values: T) => void | Promise<void>
) => async (e: React.FormEvent) => {
e.preventDefault()
const validationErrors = validate(values)
setErrors(validationErrors)
if (Object.keys(validationErrors).length === 0) {
await onSubmit(values)
}
}
return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit
}
}
// 使用例
function LoginPage() {
const initialValues: LoginForm = {
email: '',
password: '',
rememberMe: false
}
const validate = (values: LoginForm): FormErrors<LoginForm> => {
const errors: FormErrors<LoginForm> = {}
if (!values.email) {
errors.email = 'メールアドレスは必須です'
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
errors.email = '有効なメールアドレスを入力してください'
}
if (!values.password) {
errors.password = 'パスワードは必須です'
} else if (values.password.length < 8) {
errors.password = 'パスワードは8文字以上である必要があります'
}
return errors
}
const { values, errors, touched, handleChange, handleBlur, handleSubmit } =
useForm(initialValues, validate)
const onSubmit = async (values: LoginForm): Promise<void> => {
console.log('ログイン:', values)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
type="email"
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
placeholder="メールアドレス"
/>
{touched.email && errors.email && (
<span className="error">{errors.email}</span>
)}
</div>
<div>
<input
type="password"
value={values.password}
onChange={(e) => handleChange('password', e.target.value)}
onBlur={() => handleBlur('password')}
placeholder="パスワード"
/>
{touched.password && errors.password && (
<span className="error">{errors.password}</span>
)}
</div>
<label>
<input
type="checkbox"
checked={values.rememberMe}
onChange={(e) => handleChange('rememberMe', e.target.checked)}
/>
ログイン状態を保存
</label>
<button type="submit">ログイン</button>
</form>
)
}
よくあるTypeScriptの問題と解決方法
anyの乱用・nullチェックの漏れ・型アサーションの誤用という、TypeScript初心者がはまりやすい3つの落とし穴と、その正しい解決パターンを習得します。
問題1: anyを避ける
anyを使うとTypeScriptの型チェックが完全に無効化されます。
外部から来る値にはunknownを使い、型ガードで絞り込むか、ジェネリクスで型パラメータとして受け取るのが正しいアプローチです。
// ❌ 悪い例
function processData(data: any) {
return data.value
}
// ✅ 良い例:unknown + 型ガード
function processData(data: unknown) {
if (typeof data === 'object' && data !== null && 'value' in data) {
return (data as { value: string }).value
}
throw new Error('Invalid data')
}
// ✅ さらに良い例:ジェネリクス
function processData<T extends { value: string }>(data: T): string {
return data.value
}
問題2: nullチェック
nullやundefinedの可能性がある値にそのままアクセスするとコンパイルエラーになります。
if文で早期returnするか、オプショナルチェーン(?.)とNullish Coalescing(??・||)を組み合わせるのが定番の対処法です。
// ❌ 悪い例
function displayUser(user: User | null) {
return <div>{user.name}</div> // エラー
}
// ✅ 良い例1: 条件分岐
function displayUser(user: User | null) {
if (!user) return <div>ユーザーが見つかりません</div>
return <div>{user.name}</div>
}
// ✅ 良い例2: オプショナルチェーン
function displayUser(user: User | null) {
return <div>{user?.name || 'ゲスト'}</div>
}
問題3: 型アサーション
asによる型アサーションは、実際の値の型と指定した型が食い違っていてもエラーにならないため、実行時エラーの原因になります。
型ガードで実際の値の構造を確認してから使うのが安全です。
// ❌ 悪い例:強制的な型アサーション
const value = someValue as string
// ✅ 良い例:型ガードを使う
if (typeof someValue === 'string') {
const value: string = someValue
}
まとめ
この記事では、TypeScriptとReactの高度な組み合わせを詳しく学びました。
重要なポイント:
- ジェネリクスで汎用的なコンポーネントを作成
- ユーティリティ型で型を変換
- Union型で柔軟な型定義
- 型ガードで型を絞り込む
- forwardRefやContextでも型安全に
- anyを避けてunknownと型ガードを使う
TypeScriptのメリット:
- コンパイル時にエラーを検出
- エディタの補完が強力
- リファクタリングが安全
- ドキュメントとしても機能
- チーム開発で型の共有
ベストプラクティス:
- anyは使わない(unknownを使う)
- すべてのコンポーネントに型を付ける
- ジェネリクスで再利用性を高める
- ユーティリティ型を活用する
- 型ガードで安全に型を絞り込む
次のステップ:
次回は、スタイリング手法について学びます。
CSS Modules、Styled-components、Tailwind CSSなど、様々なスタイリング方法を比較し、それぞれの使い方を詳しく解説します!
TypeScriptは最初は複雑に見えますが、慣れると手放せない強力なツールです。
型安全なコードで、バグの少ないアプリケーションを作りましょう!


























