Next.js と TypeScript で、never 型を学ぶ

TwitterFacebookHatena

TL;DR

今日は「never 型」について話します。

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

never 型とは?

never 型は、TypeScript では、「絶対に何も返さない」ときに使われる特殊な型です。なんだか難しそうですね。でも、実はとても便利な型なんです。

例えば、次のような関数を考えてみてください。

function throwError(message: string): never {
  throw new Error(message)
}

この関数はエラーを投げるだけで、正常に終了することはないです。だから、この関数の戻り値の型は never になるんですね。

また、無限ループの関数も考えてみましょう。

function infiniteLoop(): never {
  while (true) {}
}

この関数は終了しないので、戻り値の型は never になります。

never 型は、他のどんな型とも互換性がない特殊な型。だから、「この関数や処理は絶対に正常に終わらない」とコード上で表現することができるんです。

void 型や never 型の違い

まず、TypeScript にはいくつかの特殊な型がありますが、その中にvoid型とnever型があります。それぞれがどのような時に使われ、どのような意味を持つのかを説明します。

void型は、主に関数が特定の値を返さないことを表すために使用されます。具体的には、関数が値を返さず、return文を使わずに終了する場合に、その関数の戻り値の型としてvoidを使います。

次にnever型ですが、この型は、void型とは少し違った意味を持ちます。never型は、関数が絶対に正常に終了しないことを表すための型です。つまり、never型の関数は、例えばエラーを投げるか、無限ループするなど、必ず何らかの形で中断されます。

それぞれの違いを、具体的なコードを見ながら理解してみましょう。

function sayHello(): void {
  console.log('Hello, world!')
  // この関数は値を返さない
}

function throwError(): never {
  throw new Error('This is an error!')
  // この関数は常にエラーを投げるため、正常に終了しない
}

上記の例で、sayHello関数は値を返さないため、戻り値の型としてvoidが指定されています。一方、throwError関数はエラーを投げるため、常に中断されることを表すnever型が戻り値として指定されています。

それぞれの型がどういったケースで使われるのか、具体的な例を見ていきましょうね。

void 型

void型は主に以下のようなケースで使われます。

ログを出力する関数: ログをコンソールに表示するだけで、特定の値を返す必要がない場合、その関数の戻り値はvoidになります。

function logMessage(message: string): void {
  console.log(message)
}

イベントハンドラ: ボタンがクリックされたときや、フォームが送信されたときなど、特定のイベントが発生したときに実行する関数(イベントハンドラ)は、多くの場合、何も返す必要がありません。

function handleClick(): void {
  alert('Button was clicked!')
}

never 型

一方、never型は以下のようなケースで使われます。

常にエラーをスローする関数: 何らかの理由で、特定の関数が呼び出されると常にエラーが発生する場合、その関数の戻り値はneverになります。

function alwaysThrowsError(): never {
  throw new Error('This function always throws an error')
}

無限ループ: 無限に処理を繰り返す関数や、特定の条件が満たされるまで処理を繰り返す関数でも、その条件が絶対に満たされないとわかっている場合、戻り値はneverになります。

function infiniteLoop(): never {
  while (true) {
    console.log('This is an infinite loop!')
  }
}

以上のように、それぞれの型は異なるケースで使われ、それぞれが異なる目的と機能を果たしています。この違いを理解することで、コードをより明確にし、意図しない動作を防ぐことができます。

これらの型を使うことで、関数の動作をより詳細に制御し、意図しない動作やエラーを防ぐことができるのです。void型とnever型は似ているように見えますが、役割と使い方が大きく異なることを覚えておきましょうね。

never 型は、どういうときに役立つのか?

never型は主に、以下のようなシーンで役立ちます。

エラーをスローする関数

never型は、関数が絶対に正常に終了しないことを示すのに役立ちます。例えば、エラーハンドリング用の関数などでよく使用されます。

function throwError(message: string): never {
  // この関数は絶対に正常に終了しない
  throw new Error(message)
}

絶対に終了しないループ

無限ループのように絶対に終了しないループもnever型を返します。

function infiniteLoop(): never {
  while (true) {
    // このループは絶対に終了しない
  }
}

型ガード

TypeScript における型ガードでは、特定の型が期待する型であることを確認するための機能を提供します。never型は、全ての型のサブタイプとして、型ガードで非常に役立ちます。

type Foo = { kind: 'foo'; foo: number }
type Bar = { kind: 'bar'; bar: number }
type Baz = Foo | Bar

function doSomething(arg: Baz) {
  switch (arg.kind) {
    case 'foo':
      return arg.foo
    case 'bar':
      return arg.bar
    default:
      const exhaustiveCheck: never = arg
      return exhaustiveCheck
  }
}

このdoSomething関数では、argFoo型かBar型かによって異なる値を返します。defaultのケースでは、exhaustiveChecknever型として定義されていて、もしFooBar以外の値が渡されるとコンパイルエラーが発生します。これによって、開発者がBaz型を更新して新たなサブタイプを追加した場合でも、それがdoSomething関数で考慮されるようになります。

これらの例からわかるように、never型はコードが特定のパスを絶対に通らないことを明示するのに役立ちます。そのため、never型はエラーハンドリング、無限ループの作成、または型ガードといった、特定の動作を示す場合に特に有用です。

Next.js で、never 型を実装

それでは、never 型を Next.js のコードでどう使うかを見てみよう。例えば、API のエラーハンドリングを考えてみましょう。

まずは、API からデータを取得する関数を作るね。エラーが発生した場合には never 型の関数を使ってエラーを投げます。

// ファイル名: api.ts
type Data = {
  id: number
  name: string
}

function handleError(response: Response): never {
  throw new Error(`Failed to fetch data: ${response.status}`)
}

async function fetchData(): Promise<Data> {
  const response = await fetch('/api/data')

  if (!response.ok) {
    handleError(response)
  }

  const data: Data = await response.json()
  return data
}

次に、React コンポーネントでこの fetchData を呼び出します。

// ファイル名: MyComponent.tsx
import { fetchData } from './api'

type Props = {
  /* props definition */
}

const MyComponent = ({}: Props) => {
  React.useEffect(() => {
    fetchData().catch((error) => console.error(error))
  }, [])

  return <div>My Component</div>
}

handleError 関数は never 型を返すので、正常に終了することはないよ。だから、もし fetchData がエラーを投げたら、それは catch でキャッチされてエラーメッセージがコンソールに表示されるんですね。

never 型を実装

ここで、もう少し複雑な使い方を見てみよう。never 型は型安全を保つための有力なツールとなります。

たとえば、すべての TypeScript の型を網羅するような処理を書くときには、never 型が役立つんです。これを "exhaustive checking"(網羅的なチェック)と呼びます。

type Animal = 'cat' | 'dog' | 'bird'

function getSound(animal: Animal): string {
  switch (animal) {
    case 'cat':
      return 'meow'
    case 'dog':
      return 'woof'
    case 'bird':
      return 'tweet'
    default:
      const unexpectedAnimal: never = animal
      throw new Error(`Unexpected animal: ${unexpectedAnimal}`)
  }
}

ここでは、Animal の型は "cat"、"dog"、"bird" の 3 つだけです。もし新しい動物を追加し忘れたら、never 型の unexpectedAnimal に割り当てられず、コンパイラがエラーを出すんです。これによって、すべての可能性を網羅したコードを書くことが強制され、バグを防げるんです。

Emotion で実装

最後に、Emotion を使って、スタイルと never 型を組み合わせる例を見てみよう。CSS のプロパティ名と値を型安全に扱うために never 型を使います。

// ファイル名: MyStyledComponent.tsx
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'

type CSSProperty = 'color' | 'background' | 'margin'
type CSSValue = 'red' | 'blue' | 'green' | '10px'

function getCSSValue(property: CSSProperty): CSSValue {
  switch (property) {
    case 'color':
      return

      'red'
    case 'background':
      return 'blue'
    case 'margin':
      return '10px'
    default:
      const unexpectedProperty: never = property
      throw new Error(`Unexpected property: ${unexpectedProperty}`)
  }
}

type Props = {
  property: CSSProperty
}

const MyStyledComponent = ({ property }: Props) => {
  const value = getCSSValue(property)
  const styles = css`
    ${property}: ${value};
  `

  return <div css={styles}>My Styled Component</div>
}

ここでは、getCSSValue 関数は CSS のプロパティ名に応じて適切な値を返すよ。もし存在しないプロパティ名を渡そうとしたら、never 型の unexpectedProperty に割り当てることができず、コンパイラがエラーを出すんです。これによって、CSS のプロパティ名と値が正しく組み合わされることを保証できます。

Next.js と TypeScript で、never 型を学ぶ