デザインについての学習メモブログ

React入門 #18 – TypeScriptとReactを組み合わせる

記事内に広告が含まれています。

React入門 #18 – TypeScriptとReactを組み合わせる

これまでの記事でTypeScriptのコード例も紹介してきましたが、この記事ではTypeScriptとReactの組み合わせについてより深く学びます。

高度な型定義、ジェネリクス、ユーティリティ型、型ガードなど、実践的なTypeScriptの使い方を詳しく解説します。

基本的な型定義の復習

コンポーネントのProps・イベントハンドラへの型付けの基本を習得します。

「なんとなく動く」コードから「型エラーをコンパイル時に検出できる」堅牢なコードへの第一歩です。

コンポーネントのProps

PropsにはTypeScriptのinterfaceを使って型を定義します。

?をつけたプロパティは省略可能(オプショナル)になり、定義されていないプロパティを渡すとコンパイルエラーになるため、使い方の誤りを早期に発見できます。

TSX
// 基本的な型定義
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などのプロパティへの安全なアクセスが保証されます。

TSX
// クリックイベント
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に渡されるアイテムの型が自動的に推論されるため、補完と型チェックの恩恵を受けられます。

TSX
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に渡すデータの型がジェネリクスで縛られているため、存在しないプロパティへのアクセスはコンパイルエラーになります。

TSX
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と推論されます。

TSX
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組み込みのユーティリティ型(PartialRequiredPickOmitRecord)を使い、既存の型を変換・派生させる手法を習得します。

型の重複定義を減らし、変更に強いコードが書けるようになります。

Partial – すべてのプロパティを省略可能に

Partial<T>はすべてのプロパティをオプショナルにした型を生成します。

更新APIの引数のように「変更したいフィールドだけを渡せる」パターンに便利です。

TSX
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>は逆にすべてのプロパティを必須にします。

オプショナルで定義した設定オブジェクトを「すべて揃っていること」を前提に処理する場面で役立ちます。

TSX
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>は指定したプロパティだけを持つ型を作ります。

一覧表示用など、全フィールドが不要なコンポーネントに渡す型を絞り込めます。

TSX
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に渡さないよう型レベルで強制できます。

TSX
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型)に対して漏れなく値を定義することを強制できます。

TSX
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'以外の文字列を渡した時点でコンパイルエラーになります。

文字列の打ち間違いによるバグをゼロにできます。

TSX
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状態を漏れなく扱えるパターンとして非常に有用です。

TSX
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'の時だけminmaxが存在するといった「型の形がtypeによって変わる」コンポーネントを型安全に実装できます。

TSX
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型など「複数の型の可能性がある値」を安全に絞り込む型ガードの書き方を習得します。

typeofin演算子・カスタム型ガード関数の使い分けができるようになります。

typeof型ガード

typeofでプリミティブ型を判定すると、if分岐の中でTypeScriptが型を自動的に絞り込みます。

string側ではtoUpperCase()、number側ではtoFixed()が使えることをコンパイラが把握してくれます。

TSX
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型のどちらかに絞り込めます。

判別子フィールドを持たない型の分岐に便利です。

TSX
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)と異なり、実際の値の構造を確認するため安全です。

TSX
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の設定など副作用があるため、最近は使われなくなっています。

TSX
// 方法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.currentHTMLInputElementとして型推論され、focus()などのメソッドに安全にアクセスできます。

TSX
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>とすることで、onClickdisabledtypeなどHTMLボタンが持つすべての標準属性を独自のProps型に継承できます。

...propsでまとめてボタンに渡せるため、スプレッド構文で属性を転送するラッパーコンポーネントに最適です。

TSX
// 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を使うと実行時エラーが投げられるため、誤用を確実に検知できます。

TSX
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と同じキーを持ちながら値がオプショナルな文字列(エラーメッセージ)になります。

これにより、存在しないフィールド名のエラーを設定しようとするとコンパイルエラーになります。

TSX
// フォームの値の型
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を使い、型ガードで絞り込むか、ジェネリクスで型パラメータとして受け取るのが正しいアプローチです。

TSX
// ❌ 悪い例
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チェック

nullundefinedの可能性がある値にそのままアクセスするとコンパイルエラーになります。

if文で早期returnするか、オプショナルチェーン(?.)とNullish Coalescing(??||)を組み合わせるのが定番の対処法です。

TSX
// ❌ 悪い例
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による型アサーションは、実際の値の型と指定した型が食い違っていてもエラーにならないため、実行時エラーの原因になります。

型ガードで実際の値の構造を確認してから使うのが安全です。

TSX
// ❌ 悪い例:強制的な型アサーション
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は最初は複雑に見えますが、慣れると手放せない強力なツールです。
型安全なコードで、バグの少ないアプリケーションを作りましょう!