[한입] 서로소 유니온 타입

TK·2023년 12월 12일
0

[강의] 한입 시리즈

목록 보기
22/59

직관적으로 타입을 좁힐 수 있도록 객체 타입을 정의하는 방법을 알아보자

서로소 유니온 타입

교집합이 없는 타입들로만 만든 유니온 타입


✏️예제 1. 회원관리 기능 웹사이트

type Admin = {
  name: string;
  kickCount: number;
};

type Member = {
  name: string;
  point: number;
};

type Guest = {
  name: string;
  visitCount: number;
};

type User = Admin | Member | Guest;

function login(user: User) {
  if ("kickCount" in user) {
    // Admim
    console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`);
  } else if ("point" in user) {
    // Member
    console.log(`${user.name}님 현재까지 ${user.point}점 모았습니다.`);
  } else {
    // Guest
    console.log(`${user.name}님 현재까지 ${user.visitCount}번 방문했습니다.`);
  }
}

→ 문제점 : 타입은 잘 좁혔으나, 다른 사람이 이 코드를 본다면 조건문만 보고 (주석이 없으면 더욱 더) 직관적으로 알 수 없음.


  • 서로소 유니온 타입 사용
    : 각 타입에tag추가
type Admin = {
  tag: "ADMIN";
  name: string;
  kickCount: number;
};

type Member = {
  tag: "MEMBER";
  name: string;
  point: number;
};

type Guest = {
  tag: "GUEST";
  name: string;
  visitCount: number;
};

// 서로소 유니온 타입이 됨
type User = Admin | Member | Guest;

function login(user: User) {
  if (user.tag === "ADMIN") {
    console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`);
  } else if (user.tag === "MEMBER") {
    console.log(`${user.name}님 현재까지 ${user.point}점 모았습니다.`);
  } else {
    console.log(`${user.name}님 현재까지 ${user.visitCount}번 방문했습니다.`);
  }
}
  • 한번 더 업그레이드 (switch case)
type Admin = {
  tag: "ADMIN";
  name: string;
  kickCount: number;
};

type Member = {
  tag: "MEMBER";
  name: string;
  point: number;
};

type Guest = {
  tag: "GUEST";
  name: string;
  visitCount: number;
};

type User = Admin | Member | Guest;

function login(user: User) {
  switch (user.tag) {
    case "ADMIN": {
      console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`);
      break;
    }
    case "MEMBER": {
      console.log(`${user.name}님 현재까지 ${user.point}점 모았습니다.`);
      break;
    }
    case "GUEST": {
      console.log(`${user.name}님 현재까지 ${user.visitCount}번 방문했습니다.`);
      break;
    }
  }
}

tag에 의해 바뀐 관계

  • tag 프로퍼티 추가 전 → 교집합이 있음

  • tag 프로퍼티 추가 후 → 교집합이 없음 (서로소 집합)
    tag의 타입이 string 리터럴이어서 교집합이 불가능해짐
    ex) tag값이 "ADMIN"이면서 동시에 "MEMBER"가 될 수 없다. (스트링 리터럴은 무조건 1개의 값만 가짐)


✏️예제 2. 비동기 작업 결과 처리

type AsyncTask = {
  state: "LOADING" | "FAILED" | "SUCCESS";
  error?: { message: string };
  response?: { data: string };
};

function processResult(task: AsyncTask) {
  switch (task.state) {
    case "LOADING": {
      console.log("로딩 중");
      break;
    }
    case "FAILED": {
      console.log(`에러 발생 : ${task.error?.message}`);  // 👉옵셔널체이닝
      break;
    }
    case "SUCCESS": {
      console.log(`성공 : ${task.response?.data}`);  // 👉옵셔널체이닝
      break;
    }
  }
}

const loading: AsyncTask = {
  state: "LOADING",
};

const failed: AsyncTask = {
  state: "FAILED",
  error: {
    message: "오류 발생 원인은...",
  },
};

const success: AsyncTask = {
  state: "SUCCESS",
  response: {
    data: "데이터 ...",
  },
};

→ 옵셔널 체이닝을 지우면 오류 발생

type AsyncTask에서 errorresponse는 선택적 프로퍼티라서 타입이 제대로 좁혀지지 않는다.

  • 해결 방법 : type AsyncTask를 3개의 타입으로 분리해서 서로소 유니온 타입으로 만듦
type LoadingTask = {
  state: "LOADING";
};
type SuccessTask = {
  state: "SUCCESS";
  response: { data: string };
};
type FailedTask = {
  state: "FAILED";
  error: { message: string };
};

type AsyncTask = LoadingTask | SuccessTask | FailedTask;

function processResult(task: AsyncTask) {
  switch (task.state) {
    case "LOADING": {
      console.log("로딩 중");
      break;
    }
    case "FAILED": {
      console.log(`에러 발생 : ${task.error.message}`);
      break;
    }
    case "SUCCESS": {
      console.log(`성공 : ${task.response.data}`);
      break;
    }
  }
}

const loading: AsyncTask = {
  state: "LOADING",
};

const failed: AsyncTask = {
  state: "FAILED",
  error: {
    message: "오류 발생 원인은...",
  },
};

const success: AsyncTask = {
  state: "SUCCESS",
  response: {
    data: "데이터 ...",
  },
};

→ 선택적 프로퍼티를 사용하는 것보다 상태에 따라서 타입들을 쪼개는 방법이 유용하다.
→ switch 케이스문 사용시 직관적이고 안전하게 타입을 좁힐 수 있다.
(taged union type)

profile
쉬운게 좋은 FE개발자😺

0개의 댓글