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
関数では、arg
がFoo
型かBar
型かによって異なる値を返します。default
のケースでは、exhaustiveCheck
はnever
型として定義されていて、もしFoo
かBar
以外の値が渡されるとコンパイルエラーが発生します。これによって、開発者が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 のプロパティ名と値が正しく組み合わされることを保証できます。