Next.js と TypeScript で、ユニオン型の使い方を学ぶ

TwitterFacebookHatena

TL;DR

このページでは、TypeScript の強力な機能である「ユニオン型」について、解説していきますね。一言で言うと、ユニオン型とは異なる型を持つ値を一つの変数が取り扱えるようにするための方法です。

開発環境 バージョン
Next.js 13.4.4
TypeScript 5.0.4
Emotion 11.11.0
React 18.2.0

ユニオン型とは?

TypeScript のユニオン型とは、複数の型を一つの変数が取り扱えるようにする機能であり、これにより柔軟なプログラミングが可能になります。型の異なる複数の値を一つの変数で管理できるという考え方が、ユニオン型の根底にあります。例えば、以下のようなコードが考えられます。

type StringOrNumber = string | number

let unionVar: StringOrNumber

unionVar = 'Hello World'
console.log(unionVar) // "Hello World"

unionVar = 123
console.log(unionVar) // 123

上記の例では、StringOrNumber というユニオン型を定義しています。この型は、string または number のどちらかの型を取ることができます。そのため、unionVar という変数は、文字列でも、数値でも問題なく代入でき、その時点での値の型がその後の振る舞いを決定します。

これがユニオン型の基本的な使い方です。しかし、実際の開発ではもう少し複雑なケースが多いです。例えば、オブジェクトのプロパティが特定の型の場合に他のプロパティの型が変わるといった状況もあります。

ユニオン型とリテラル型の組み合わせ

TypeScript の力強さは、それ自体の型システムだけでなく、その型がどのように組み合わされるかにも由来しています。例えば、ユニオン型とリテラル型を組み合わせることで、特定の文字列または数値のセットのみを受け入れる型を作成できます。

以下に、リテラル型とユニオン型を組み合わせた例を示します。この例では、'red'、'green'、'blue'のいずれかの値のみを受け入れるColor型を作成しています。

type Color = 'red' | 'green' | 'blue'

const setColor = (color: Color) => {
  console.log(`The color is ${color}`)
}

このsetColor関数は、Color型の値、つまり'red''green''blue'のいずれかを引数として受け取ります。それ以外の値を受け取ろうとすると、TypeScript はエラーを出してくれます。これは、コードの保守性を高め、エラーを事前に防ぐ一助となります。

ユニオン型と型ガード

TypeScript では、「型ガード」という考え方があります。型ガードは、あるスコープ内で変数の型を確認する機能で、ユニオン型と組み合わせると非常に強力なツールになります。

以下に、ユニオン型と型ガードを組み合わせた例を示します。この例では、formatInput関数はstring型かnumber型の値を受け入れ、それぞれ異なる処理を行います。

type InputType = string | number

const formatInput = (input: InputType) => {
  if (typeof input === 'string') {
    return input.toUpperCase()
  } else {
    return input.toFixed(2)
  }
}

このformatInput関数では、引数inputが文字列か数値かをtypeof演算子を使用してチェックしています。これにより、同一の関数内で型に応じた異なる処理を行うことができます。このような型チェックを「型ガード」と呼びます。

複数のコンポーネントをまとめて型定義する

まずは、React コンポーネントにおけるユニオン型の使用例です。以下の例では、複数のコンポーネントをまとめて型定義し、一つのコンポーネントとして扱うことができます。

import React, { ReactNode } from 'react'

// コンポーネントの型を定義
type ButtonProps = {
  variant: 'button'
  onClick: () => void
  children: ReactNode
}

type LinkProps = {
  variant: 'link'
  href: string
  children: ReactNode
}

// これらをユニオン型でまとめる
type ButtonOrLinkProps = ButtonProps | LinkProps

// コンポーネントの定義
const ButtonOrLink = (props: ButtonOrLinkProps): JSX.Element => {
  if (props.variant === 'button') {
    // ButtonPropsとして扱う
    return <button onClick={props.onClick}>{props.children}</button>
  } else {
    // LinkPropsとして扱う
    return <a href={props.href}>{props.children}</a>
  }
}

export default ButtonOrLink

このコードでは、ButtonOrLinkProps というユニオン型が ButtonPropsLinkProps を組み合わせたものとして定義されています。これにより、ButtonOrLink コンポーネントは button プロパティと link プロパティの両方を持つことができ、それぞれ違った振る舞いをします。

Redux の Action をユニオン型で扱う

さらに、Redux のアクションの型をユニオン型で管理する例も見てみましょう。

// actions/index.ts

// 各アクションの型を定義
type AddTodoAction = {
  type: 'ADD_TODO'
  payload: { id: number; text: string }
}

type ToggleTodoAction = {
  type: 'TOGGLE_TODO'
  payload: { id: number }
}

// ユニオン型でまとめる
type Action = AddTodoAction | ToggleTodoAction

// reducerの定義
function todoReducer(state = [], action: Action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, action.payload]
    case 'TOGGLE_TODO':
      return state.map((todo) => (todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo))
    default:
      return state
  }
}

このコードでは、Action というユニオン型が AddTodoActionToggleTodoAction を組み合わせたものとして定義されています。これにより、todoReducerADD_TODO アクションと TOGGLE_TODO アクションの両方を管理することができ、それぞれ違った振る舞いをします。

これらの例を通じて、TypeScript のユニオン型はプログラムの柔軟性と安全性を高める強力なツールであることが理解いただけると幸いです。

Next.js で、ユニオン型を活用

それでは、Next.js のコード内でユニオン型をどのように活用できるのかについて、具体的なソースコードを通じて説明していきましょう。

まず、ユニオン型を利用して、異なるタイプの通知を表現する Notification コンポーネントを作成します。

// components/Notification.tsx

import React from 'react'

type NotificationProps = {
  message: string
  type: 'success' | 'error' | 'info'
}

const Notification = ({ message, type }: NotificationProps) => {
  let backgroundColor
  switch (type) {
    case 'success':
      backgroundColor = 'green'
      break
    case 'error':
      backgroundColor = 'red'
      break
    case 'info':
    default:
      backgroundColor = 'blue'
  }

  return <div style={{ backgroundColor }}>{message}</div>
}

export default Notification

上記の NotificationProps 型では、type プロパティが 'success''error''info' のいずれかの値を持つことを示しています。これは文字列のユニオン型を用いて表現されています。

次に、この Notification コンポーネントを呼び出す方法を見てみましょう。

// pages/index.tsx

import React from 'react'
import Notification from '../components/Notification'

const Home = () => {
  return (
    <div>
      <Notification message="操作が成功しました!" type="success" />
      <Notification message="エラーが発生しました!" type="error" />
      <Notification message="新しい機能が追加されました!" type="info" />
    </div>
  )
}

export default Home

ユニオン型を活用した型の絞り込み

さらに、TypeScript のユニオン型を活用すると、より堅牢なコードを書くことができます。以下に、複数の型のユニオン型を絞り込むための型ガードを使用した例を示します。

// utils/typeGuards.ts

type User = {
  name: string
  age: number
}

type Admin = {
  name: string
  privileges: string[]
}

type UnknownEmployee = User | Admin

function printEmployeeInformation(emp: UnknownEmployee) {
  console.log('Name: ' + emp.name)
  if ('privileges' in emp) {
    console.log('Privileges: ' + emp.privileges.join(', '))
  }
  if ('age' in emp) {
    console.log('Age: ' + emp.age)
  }
}

printEmployeeInformation({ name: 'Hiroshi', age: 20 })
printEmployeeInformation({ name: 'Riko', privileges: ['create-server'] })

このコードでは、UnknownEmployee 型は User 型と Admin 型のユニオン型です。この型の変数を引数として受け取る printEmployeeInformation 関数では、in 演算子を使用して、引数が User 型か Admin 型かを判定し、それに応じて異なる処理を行っています。

Next.js と TypeScript で、ユニオン型の使い方を学ぶ