유니온 타입(Union Type)

유니온 타입이란, 자바스크립트의 OR 연산자(||)와 같은 의미를 갖는다.

function union (name: string | Number) {
    console.log(typeof name);
}

union('10') // string
union(10) // number

위 함수에 유니온 타입을 적용하면 파라미터 name에는 문자열숫자 타입 모두 올 수 있다. 이처럼 | 연산자를 이용하여 타입을 여러 개 연결하는 방식을 유니온 타입 정의 방식이라고 부른다.

유니온 타입(Union Type)을 사용하는 이유

유니온 타입을 사용하는 이유를 코드를 보면 이해하기 쉽다.

function padLeft(value: string, padding: any) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${typeof padding}'.`);
}
 
padLeft("Hello world", 4); // returns "    Hello world"
padLeft("Hello world", '4'); // returns "    Hello world"
padLeft("Hello world", true); // Expected string or number, got 'boolean'.

위의 코드를 보면 파라미터 padding에는 stringnumber 타입 둘다 들어올 수 있기 떄문에 any 타입으로 지정했다. 하지만, 문제는 string, number 타입 이외에 타입이 들어오면 에러를 발생시킨다.

이럴때! 아래와 같이 유니온 타입을 지정해서 사용하면 문제를 해결할 수 있다.

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${typeof padding}'.`);
}
 
padLeft("Hello world", true); // 'boolean' 형식의 인수는 'string | number' 형식의 매개 변수에 할당될 수 없습니다.


공통 타입이 있는 유니온(Union Type)

유니온 타입을 사용할 때 주의해야할 점은 만약 공통적인 타입 유형이 있는 경우에는 모든 유형에 대해 유효한 경우에만 엑세스할 수 있다는 것이다.

아래의 코드를 보면 더 쉽게 이해할 수 있다.

예제1

interface person {
  name(): void;
  age(): void;
}
 
interface person2 {
  name(): void;
  address(): void;
}
 
declare function getSmallPet(): person | person2;
 
let human = getSmallPet();
human.name();
 
// Only available in one of the two possible types
human.address(); // type error

OR 조건이라며? 근데 왜 공통적인 멤버만 엑세스할 수 있는거지?! 라고 생각할 수 있다. 나도 처음엔 전부 사용할 수 있는 줄 알았지만, 타입스크립트는 불확실한 타입에 대해서는 모두 에러로 처리한다.

반대로 생각해보면, age만 들어올 수도 있고 addres만 들어올 수 있는 가능성이 있으므로 타입스크립트는 이러한 애매한 타입에 대해서는 과감하게 배재시키는 것 같다. 따라서, 타입스크립트에서는 좀더 직관적인 관점으로 바라볼 필요가 있다.

예제2

아래 예제 코드도 마찬가지로 Union의 모든 타입이 유효한 경우에만 작업을 허용한다. 예를 들어, string 타입에서만 사용할 수 있는 메서드를 number | string에서는 사용할 수 없다.

function printId(id: number | string) {
  console.log(id.toUpperCase());
}

// 'string | number' 형식에 'toUpperCase' 속성이 없습니다.
// 'number' 형식에 'toUpperCase' 속성이 없습니다.

해결책

해결책은 코드와의 결합을 좁히는 것이다. 좁히는 방법은 타입스크립트의 코드 구조를 기반으로 값에 대해 보다 구체적인 유형을 추론할 수 있도록 해주는 것이다.

예를 들어 타입스크립트 값에는 오직 string이 있다는 것을 알고 싶다면 typeof "string" 조건을 사용하는 것이다.

function printId(id: number | string) {
  if (typeof id === "string") {
    console.log(id.toUpperCase());
  } else {
    // 여기는, id는 number 타입이다.
    console.log(id)
  }
}

printId('string') // STRING
printId(100) // 100

이처럼 해당 타입을 확실히 허용할 수 있는 조건을 걸어주면 해당 타입으로 작업을 진행할 수 있다.


Discriminating Unions

공용체 작업을 위한 일반적인 기술은 타입스크립트가 현재 유형을 좁히는 데 사용할 수 있는 리터럴 타입을 사용하는 단일 필드를 갖는 것이다.

예를 들어, state라는 단일 공유 필드가 있는 세 가지 유형의 공용체를 만들 것이다.

type LoadingState = {
  state: 'loading';
};

type FaildState = {
  state: 'failed';
  code: number;
};

type SuccessState = {
  state: 'success';
  response: {
    title: string;
    duration: number;
    summary: string;
  }
}

type workState = 
  | LoadingState
  | FaildState
  | SuccessState;

위의 모든 유형에는 state라는 공통의 필드가 있으며 고유한 필드도 존재한다.

LoadingState FaildState SuccessState
state state state
  code response

state 필드가 모든 유형에서 공통적으로 들어있다는 것을 감안했을 때, workState 코드에서는 존재 유무 확인없이 엑세스하는 것이 안전하다.

state를 리터럴 타입으로 사용하면, 해당 문자열과 state값을 비교할 수 있다. 또한, state는 현재 타입스크립트에서 사용 중인 유형인 것을 알 수 있다.

LoadingState FaildState SuccessState
“loading” “failed” “success”

이 경우, switch문을 사용하여 런타임에 표시되는 유형을 좁힐 수 있다.

type workState = 
  | LoadingState
  | FaildState
  | SuccessState;


function logger(state: workState): string {
  
  switch (state.state) {
    case "loading":
      return "Downloading...";
    case "failed":
      return `Error ${state.code} downloading`;
    case "success":
      return `Downloaded ${state.response.title} - ${state.response.duration} - ${state.response.summary}`;
  }
}
const loading: workState = {state:'loading'};
const failed: workState = {state:'failed', code:200};
const success: workState = {state:'success', response:{title:'test', duration:10, summary:'success!!!'}};

console.log(logger(loading)) // Downloading...
console.log(logger(failed))  // Error 200 downloading
console.log(logger(success)) // Downloaded test - 10 - success!!!


인터섹션 타입(Intersection Type)

인터섹션 타입은 유니온 타입과 다르게 어려 타입을 모두 만족하는 하나의 타입을 의미한다.

아래의 코드를 보자.

interface NetError {
  success: boolean;
  error?: { message: string };
}

interface networking {
  name: string,
}

type networkingResponse = networking & NetError;

const handleNetworking = (response: networkingResponse) => {
  if (response.error) {
    console.error(response.error.message);
    return ;
  }

  console.log(response.name)
}

const error: networkingResponse =  { success: false, error: {message: '500'}, name: '통신'}
const success: networkingResponse =  { success: true, name: '통신'}

// error
handleNetworking(error) // 500
//success
handleNetworking(success) // 통신
{
  success: boolean;
  error?: { message: string };
  name: string,
}

이처럼 & 연산자를 이용해 여러 개의 타입 정의를 하나로 합치는 방식을 인터섹션 타입 정의 방식이라고 한다.