🚀

TypeScriptのnever型の使い所

に公開

概要

TypeScriptのnever型の使い所について説明いたします。

never型とは

never型は「値を持たない」を意味するTypeScriptの特別な型です。

https://typescriptbook.jp/reference/statements/never

以下のような使用をすれば何も代入できません。しかし実際には以下のようなコードを書くことはないはずです。

const foo: never = 1;

例外を投げる処理に使う

例外を投げる処理に使用可能です。しかし、TSには型推論があるので必須ではありません。

const throwError = (): never => {
  throw new Error();
};

以下のようにvoidにする事も可能ですが、誤解を招く危険なコードになっていまいます。

const badFunction = (): void => {
  throw new Error();
};

例外を投げることを知るには、badFunctionの実装を見る必要がある問題が発生します。
チームでの開発時にすべてのコードを確認することは不可能なので実装ではなく型で伝えてあげると良いと思います。

const badFunction = (): void => {
  throw new Error();
};

const main = () => {
  // 例外を投げることを知るには、badFunctionの実装を見る必要がある
  badFunction();
};

網羅性チェックに使う

TypeScriptのユニオン型と組み合わせて、型安全なコードを書くことも可能です。
値を持たない特性を使用したコードです。

type Extension = 'js' | 'ts' | 'json';

const mainWithNever = (extension: Extension) => {
  if (extension === 'js') {
    return 'goodJS';
  }

  if (extension === 'ts') {
    return 'goodTS';
  }

  if (extension === 'json') {
    return 'goodJSON';
  }

  // ここに到達する事がないので、extensionは、neverとなる
  const neverExtension: never = extension;
  
  // satisfiesを使用しても同等の処理は可能
  extension satisfies never;
  
  return;
};

fetchなどのPromiseと組み合わせる

fetch処理は、例外を投げる可能性があります。以下のような場合 try catchが必要な事に気づきづらいです。

const vanillaFetch = (): Promise<Response> => fetch('https://dummy.com');

const mainWithVanillaFetch = async () => {
  await vanillaFetch();
};

プロダクションのコードでは、以下のようにする必要があります。

const mainWithVanillaFetch = async () => {
  try {
    await vanillaFetch();
  } catch (e) {
    // 例外のハンドリング
  }
};

Promise<Response | never>型にする事でexplicitFetchの処理を確認せずに、try catchが必要な事に気が付きます。
型情報やメソッド名で処理の概要が理解できれば、コードリーディングの補助になると思います。

const explicitFetch = (): Promise<Response | never> =>
  fetch('https://dummy.com');

その結果以下のようなコードを書きやすくなります。

const explicitFetch = (): Promise<Response | never> =>
  fetch('https://dummy.com');

const mainWithExplicitFetch = async () => {
  try {
    // 例外を投げる可能性があるならば、try catchで囲む
    await explicitFetch();
  } catch (e) {
    // 例外のハンドリング
  }
};

neverを使わないfetch

try catchを多用するとネストが深くなりどこでエラーが発生したかわかりづらくなります。then catchを付与したfetchを作りnever try catchを使用しない方法もあります。

type Success = {
  success: true;
  data: 'ok';
};

type Failure = {
  success: false;
  error: unknown;
};

const safetyFetch = (): Promise<Success | Failure> =>
  fetch('https://dummy.com')
    .then(() => ({ success: true, data: 'ok' }) as const)
    .catch(
      (error) =>
        ({
          success: false,
          error,
        }) as const,
    );

const mainWithSafetyFetch = async () => {
  const res = await safetyFetch();

  if (!res.success) {
    // 失敗時のハンドリング
    return;
  }

  // 安全に値へのアクセスが可能
  res.data satisfies 'ok';
};

Success Failureをハードコーディングするとコードの一貫性が保てなくなります。SafeResponseのような抽象的な型を作るのが良いと思います。

type SafeResponse<T, U = unknown> =
  | { success: true; data: T }
  | { success: false; error: U };

type SomeRes = SafeResponse<'ok'>;
type UserRes = SafeResponse<{ id: string; name: string }>;
chot Inc. tech blog

Discussion