💡

Union Typeをfunctionの戻り値にする

に公開

先日、ざっくり表現すると、下記のようなコードに出会いました。

import axios from "axios";

type User = {
  id: number;
  name: string;
};

type FetchUserResponse =
  | {
      status?: number;
      error: unknown | null;
    }
  | { status?: number; data: User };

async function fetchUser(): Promise<FetchUserResponse | null> {
  try {
    const { data, status } = await axios.get<User>("/user?ID=12345");
    return { data, status };
  } catch (error) {
    if (axios.isAxiosError(error)) {
      return { status: error?.status, error: error };
    } else {
      throw error;
    }
  }
}

fetchUser().then((res) => {
  // Property 'data' does not exist on type '{ status?: number | undefined; error: unknown; }'
  res?.data;

  // Property 'error' does not exist on type '{ status?: number | undefined; data: User; }'
  res?.error;

  res?.status;
});

ちょっと分かりづらいですが、FetchUserResponseがUnion TypeでfetchUser()関数の戻り値の方として設定されています。

axios.getの結果が正常なら、戻り値にdataが存在して、エラー(http status400番台とか500番台)ならdataが無くて、errorが存在するということを表してそうです。

リテラル型のUnion Typeと同じ感覚だといけそうに思いますが、上記のコード中のコメントにもある通り、typescript的にはエラーになるようです。
↓抜粋

fetchUser().then((res) => {
  // Property 'data' does not exist on type '{ status?: number | undefined; error: unknown; }'
  res?.data;

  // Property 'error' does not exist on type '{ status?: number | undefined; data: User; }'
  res?.error;

  // 両方の型に存在するstatusにアクセスする場合はエラーにならない
  res?.status;
});

こちらにまさにそのままな説明が載っていました。

自分が書いたコードの例で言うと、resが、文脈からdataを持つ型なのか、errorを持つ型なのか、コンパイラが判別できないと言うことになるかと思います。

上記の参照元を参考にして書き換えるとこんな感じになるかと思います。

// 変わったところだけ抜粋
type FetchUserResponse =
  | {
      status?: number;
      error: unknown | null;
      type: "Failure";
    }
  | { status?: number; data: User; type: "Success" };

async function fetchUser(): Promise<FetchUserResponse | null> {
  try {
    const { data, status } = await axios.get<User>("/user?ID=12345");
    return { data, status, type: "Success" };
  } catch (error) {
    if (axios.isAxiosError(error)) {
      return { status: error?.status, error: error, type: "Failure" };
    } else {
      throw error;
    }
  }
}

fetchUser().then((res) => {
  switch (res?.type) {
    case "Success":
      res.data;
      res.status;
      break;
    case "Failure":
      res.error;
      res.status;
      break;
    default:
      console.log("不正なタイプ");
  }
});

これでエラーは出なくなりました。

でもまあ、下記でも不便なく使えそうではあります。

type FetchUserResponse = {
  status?: number;
  error?: unknown | null;
  data?: User;
};

async function fetchUser(): Promise<FetchUserResponse | null> {
  try {
    // urlは存在しないので、実行できません。
    const { data, status } = await axios.get<User>("/user?ID=12345");
    return { data, status };
  } catch (error) {
    if (axios.isAxiosError(error)) {
      return { status: error?.status, error: error };
    } else {
      throw error;
    }
  }
}

fetchUser().then((res) => {
  // 全部エラーにならない
  res?.data;
  res?.error;
  res?.status;
});

javascriptは存在しないプロパティにアクセスしても、異常終了したりしないので、↑で十分な気もしました。

しくみのテックブログ

Discussion