서로소 유니온 타입

교집합이 없는 타입들로만 만든 유니온 타입을 말한다.

태그를 붙혀서 객체를 구별할 수 있는 기능이기 때문에 태그드유니온타입 이라고도 한다.

서로소집합

string | number 와 같이 공통원소가 하나도 없는 관계를 수학에서는 서로소 관계에 있다고 한다.

이런 두개의 집합을 서로소 집합이라 한다.

아래 코드는 주석이 없을 경우 login 함수쪽만 보고서는 어떤 조건인지 직관적으로 알기가 어렵다.

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) { // 타입좁히기
    // kickCount in user라는 조건만 보고 이 부분이 admin타입으로 좁혀졌다고 알기가 쉽지않다.
        // admin 타입
        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}번 방문했습니다.`)
    }
}

개선코드

각각의 타입에 태그라는 프로퍼티를 리터럴 타입으로 추가한 후, 서로소관계에 있는 타입들을 유니온 타입으로 묶어 User2타입을 만들어서 사용하였다.

type Admin2 = {
    tag:'ADMIN';
    name: string;
    kickCount: number;
};
type Member2 = {
    tag:'MEMBER';
    name: string;
    point: number;
};
type Guest2 = {
    tag:'GUEST';
    name: string;
    visitCount: number;
};

type User2 = Admin2 | Member2 | Guest2;

function login2(user:User2) {
    if(user.tag ==='ADMIN') { // 타입별 tag 프로퍼티로 조건을 기술하니 더욱 직관적게 되었다.
        // admin2 타입
        console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`)
    } else if (user.tag ==='MEMBER') {
        // Member2 타입
        console.log(`${user.name}님 현재까지 ${user.point}모았습니다.`)
    } else {
        // Guest2 타입
        console.log(`${user.name}님 현재까지 ${user.visitCount}번 방문했습니다.`)
    }
	
		// 혹은 더 직관적으로 switch문을 사용할 수도 있다.
    switch(user.tag){
        case "ADMIN" : {
            // admin2 타입
            console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`)
            break;
        }
        case "MEMBER" : {
            // Member2 타입
            console.log(`${user.name}님 현재까지 ${user.point}모았습니다.`)
            break;
        }
        case "GUEST" : {
            // Guest2 타입
            console.log(`${user.name}님 현재까지 ${user.visitCount}번 방문했습니다.`)
            break;
        }   
    }
}

서로소유니온타입의 사용

아래 코드는 비동기 작업결과를 처리하기위해 만든 객체이다.

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

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

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

const success: AsyncTask = {
    state: 'SUCCESS',
    response: {
        data: '데이터~'
    }
}

문제는 아래 코드에 task.error.message처럼 접근했을때 task가 정상적으로 좁혀지지 않아서 에러가 난다. task.error의 프로퍼티가 선택적 프로퍼티로 되어있기 때문에 task에 에러가 있는지, 없는지 확실하게 알 수 없는 상황이라 그렇다.

function processResult(task: AsyncTask) {
    switch(task.state) {
        case "LOADING" : {
            console.log('로딩중');
            break
        }
        case "FAILED" : {
		        // 이때 task의 타입은 AsyncTask이다. failed타입으로 좁혀지지 않았다.
						**console.log(`에러발생 : ${task.error.message}`);**
            break;
        }
        case "SUCCESS" : {
            console.log(`성공 : ${task.response?.data}`);
            break;
        }
    }
}

그래서 부득이하게 옵셔널체이닝이나 non null단언을 사용해줘야하는데 이는 타입스크립트를 사용할때 그다지 안전한 코드가 아니라고 할 수 있다.

// 옵셔널 체이닝을 사용하거나
console.log(`에러발생 : ${task.error?.message}`);
// non null 단언을 사용해야하는 상황이다.
console.log(`에러발생 : ${task.error!.message}`);
break;

서로소유니온타입으로 만들어서 안전한 코드로 만들기