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
というユニオン型が ButtonProps
と LinkProps
を組み合わせたものとして定義されています。これにより、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
というユニオン型が AddTodoAction
と ToggleTodoAction
を組み合わせたものとして定義されています。これにより、todoReducer
は ADD_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
型かを判定し、それに応じて異なる処理を行っています。