TypeScriptでのnever型の利用例(網羅性チェックとUnion型のフィルタリング)

JavaScript

never型とは『けっして起こりえないこと』を表現する型です。
けっして発生しない事象に対してnever型が付与されるので、never型にはどんな値もセットできません。

今回はnever型の活用事例について紹介します。

Switch文やif文における網羅性のチェック

never型は『あり得ない』を表現するため、どんな値もnever型へセットできません。
この特性を応用することで網羅性をチェックできます。

利用するサンプルコード

正方形、長方形、三角形の面積を求めるareaメソッドを作る場合を考えます。
areaの実行結果は以下のようなイメージです。

type Square = {
    kind: "square";
    size: number;
}

type Rectangle = {
    kind: "rectangle";
    width: number;
    height: number;
}

type Triangle = {
  kind: "triangle";
  width: number;
  height: number;
}

type Shape = Square | Rectangle | Triangle;

const square: Square = { kind: "square", size: 5 }
const rectangle: Rectangle = { kind: "rectangle", width: 5, height: 4 }
const triangle: Triangle = { kind: "triangle", width: 5, height: 4 }

console.log(area(square)); // 25
console.log(area(rectangle)); // 20
console.log(area(triangle)); // 10

考慮漏れが実行時にならないと気がつかない例

以下の実装はTriangle型の考慮が漏れている実装です。

const area =(shape: Shape) => {
  if (shape.kind === "square") {
      return shape.size * shape.size;
  } else if (shape.kind === "rectangle") {
      return shape.width * shape.height;
  } else {
    throw new Error(`Not Shape.`);
  }
}

上記の場合、考慮漏れに気がつくのはTriangle型の変数をメソッドに渡した実行時です。
コード自体は間違っていないため、コンパイルエラーで事前検知できません。

// 実行時になって初めて例外が発生するのに気がつく
console.log(area(triangle)); // Error: Not Shape.

neverを利用することで考慮漏れがあった場合はコンパイル時に気がつける

以下の実装も先ほどと同様、Triangle型の考慮が漏れている実装です。違いはelse節でnever型に値をセットするコードを追加している点です。
never型にはどんな値もセットできないため、以下のコードはコンパイルエラーになります。

const area = (shape: Shape) => {
  if (shape.kind === "square") {
      return shape.size * shape.size;
  } else if (shape.kind === "rectangle") {
      return shape.width * shape.height;
  } else {
    // Triangle型の場合、shapeがnever型にセットされるのでコンパイルエラーになる
    const _exhaustiveCheck: never = shape; // Type 'Triangle' is not assignable to type 'never'.ts(2322)
    throw new Error(`Not Shape.`);
  }
}

このように、never型を利用することで考慮漏れによるエラーを事前検知できます。

Union型(Union Types)をフィルタリングするConditional Typesの作成

Conditional Typesとnever型を組み合わせることで、条件を満たした型のみUnion型に含めるというフィルタリング機能を実装できます。

Conditional Typesについて

Conditional Typesとは条件分岐の概念がある型のことをいいます。
Conditional TypesはT extends U ? A : Bのように記述し、『型Tが型Uを満たす場合は型A、満たさない場合は型B』となります。

例えば『型Tがstring型を満たす場合はtrue型、満たさない場合はfalse型』をConditional Typesで表現するとT extends string ? true : falseとなります。
具体的な利用方法は以下の通りです。

// IsString<T>型は『型Tがstring型を満たす場合はtrue型、満たさない場合はfalse型』というConditional Types
type IsString<T> = T extends string ? true : false

// 'a'はstring型を満たすのでtrue型
type A = IsString<'a'>
// type A = true


// 1はstring型を満たさないのでfalse型
type B = IsString<1>
// type B = false

// booleanはstring型を満たさないのでfalse型
type C = IsString<boolean>
// type C = false

Union型におけるnever型の特性について

never型にはどんな値もセットできないため、T | neverというUnion型はTになります。
具体的には以下の通りです。

type Foo = string | never
// type Foo = string と型推論される

Conditional Typesとnever型を組み合わせた、Union型のフィルタリングの具体例

Union型のフィルタリングには、never型を利用したT extends U ? T : neverというConditional Typesを利用します。
T extends U ? T : neverは『型Tが型Uを満たす場合は型T、満たさない場合はnever型』を表現します。つまり、『型Uを満たさなければ型Tをnever型として扱う』と言い換えられます。
Union型において、型Uを満たさない型Tは除外されることになります。

具体的なコードは以下の通りです。

type Square = {
  kind: "square";
  size: number;
}

type Rectangle = {
  kind: "rectangle";
  width: number;
  height: number;
}

type Triangle = {
  kind: "triangle";
  width: number;
  height: number;
}

type FourSidedShape = Square | Rectangle;

// FourSidedShapeを満たす型のみをフィリタリングする Conditional Types
type FourSidedShapeFilter<T> = T extends FourSidedShape ? T : never

// Triangleは FourSidedShape を満たさないので FourSidedShapeTypeから除外されている
type FourSidedShapeType = FourSidedShapeFilter<Square | Rectangle | Triangle>
// type FourSidedShapeType = Square | Rectangle

type FourSidedShapeType = FourSidedShapeFilter<Square | Rectangle | Triangle>type FourSidedShapeType = Square | Rectangleになる理由について説明します。

FourSidedShapeFilter<Square>SquareFourSidedShapeFilter<Rectangle>RectangleFourSidedShapeFilter<Triangle>neverになります。
ですので、FourSidedShapeFilter<Square | Rectangle | Triangle>Square | Rectangle | neverというUnion型になります。
Union型においてnever型は無視されるので最終的にはSquare | RectangleというUnion型になります。

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考