직관적으로 타입을 좁힐 수 있도록 객체 타입을 정의하는 방법을 알아보자
교집합이 없는 타입들로만 만든 유니온 타입
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}번 방문했습니다.`);
}
}
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
의 타입이 string 리터럴
이어서 교집합이 불가능해짐
ex) tag값이 "ADMIN"이면서 동시에 "MEMBER"가 될 수 없다.
(스트링 리터럴은 무조건 1개의 값만 가짐)
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
에서 error
와 response
는 선택적 프로퍼티라서 타입이 제대로 좁혀지지 않는다.
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)