👶 TypeScript

정제 - 차별된 유니온 타입

개발자 린다씨 2023. 1. 19. 12:00
반응형

차별된 유니온 타입

TypeScript는 JavaScript가 어떻게 동작하는지 잘 이해하며, 마치 프로그래머가 머리로 프로그램을 추적하듯이 코드로부터 타입을 정제해 낼 수 있습니다.

 

예를 들어 응용 프로그램의 커스텀 이벤트 시스템을 만든다고 가정합니다.

 

먼저 몇 가지 이벤트 타입과 이벤트들을 처리할 함수를 정의합니다.

 

KeyBoardEvent는 키보드 이벤트를, MouseControlEvent는 마우스 이벤트를 가리킵니다.

type KeyBoardEvent = {value: string}
type MouseControlEvent = {value: [number, number]}

type FirstEvent = KeyBoardEvent | MouseControlEvent

function handle(event: FirstEvent){
    if(typeof event.value === 'string'){
        event.value // string
        // 블라 블라
        return
    }
    event.value // [number, number]
}

if 블록 내부에서 TypeScript는 (typeof로 확인했으므로) event.value가 문자열임을 알고 있습니다.

 

따라서 if 블록 이후의 event.value는 [number, number] 튜플이어야 합니다.

 

조금 더 복잡해지면 무슨 일이 일어날까요?

 

이벤트 타입에 정보를 더 추가하면서 타입을 정제할 때 TypeScript가 어떻게 처리되는지 확인해 보겠습니다.

type KeyBoardEvent = {value: string, target: HTMLInputElement}
type MouseControlEvent = {value: [number, number], target: HTMLElement}

type FirstEvent = KeyBoardEvent | MouseControlEvent

function handle(event: FirstEvent){
    if(typeof event.value === 'string'){
        event.value // (property) value: string
        event.target // (property) target: HTMLInputElement | HTMLElement
        // 블라 블라
        return
    }
    event.value // (property) value: [number, number]
    event.target // (property) target: HTMLInputElement | HTMLElement
}

event.value는 잘 정제되었지만 event.target에는 적용되지 않았습니다.

 

이유가 뭘까요?

 

handle이 FirstEvent 타입의 매개변수를 받는다는 것은 KeyBoardEvent나 MouseControlEvent만 전달할 수 있다는 의미가 아닙니다.

 

사실 KeyBoardEvent | MouseControlEvent 타입의 인수를 전달할 수도 있습니다.

 

유니온의 멤버가 서로 중복될 수 있으므로 TypeScript는 유니온의 어떤 타입에 해당하는지를 조금 더 안정적으로 파악할 수 있어야 합니다.

 

리터럴 타입을 이용해 유니온 타입이 만들어낼 수 있는 각각의 경우를 태그(tag)하는 방식으로 이 문제를 해결할 수 있습니다.

 

아래는 좋은 태그의 조건입니다.

  •  유니온 타입의 각 경우와 같은 위치에 있습니다. 객체 타입의 유니온에선 같은 객체 필드를 의미하고, 튜플 타입의 유니온이라면 같은 인덱스를 의미합니다. 보통 태그 된 유니온은 객체 타입을 사용합니다.
  • 리터럴 타입입니다. 다양한 리터럴 타입을 혼합하고 매치할 수 있지만 한 가지 타입만 사용하는 것이 바람직합니다. 보통은 문자열 리터럴 타입을 사용합니다.
  • 제네릭이 아닙니다. 태그는 제네릭 타입 인수를 받지 않아야 합니다.
  • 상호 배타적입니다.(ex. 유니온 타입 내에서 고유함)

이를 생각하면서 이벤트 타입을 다시 바꿔보겠습니다.

type KeyBoardEvent = {type: 'KeyBoard', value: string, target: HTMLInputElement}
type MouseControlEvent = {type: 'MouseControl', value: [number, number], target: HTMLElement}

type FirstEvent = KeyBoardEvent | MouseControlEvent

function handle(event: FirstEvent){
    if(event.type === 'KeyBoard'){
        event.value // (property) value: string
        event.target // (property) target: HTMLInputElement
        // 블라 블라
        return
    }
    event.value // (property) value: [number, number]
    event.target // (property) target: HTMLElement
}

event를 태그 된 필드(event.type) 값에 따라 정제하도록 수정했으므로, TypeScript는 if 문에서는 event가 HTMLInputElement여야 하며 if 문 이후로는 HTMLElement여야 한다는 사실을 알게 됩니다.

 

태그는 유니온 타입에서 고유하므로 TypeScript는 둘이 상호 배타적임을 알 수 있습니다.

 

유니온 타입의 다양한 경우를 처리하는 함수를 구현해야 한다면 태그 된 유니온을 사용합시다:)

반응형