다음과 같은 타입이 있다고 해보자.
type Shape = 'SQUARE' | 'TRIANGLE' | 'CIRCLE'
`Shape` 타입은 3개의 문자열(`SQUARE`, `TRIANGLE`, `CIRCLE`)만 허용하도록 하는 문자열 리터럴 타입이다.
그런데 이때 (조건 1) 3개의 문자열에 대해 타입 힌트는 주고 싶지만, (조건 2) 3개의 문자열에 속하지 않는 다른 문자열도 허락하고 싶다면 어떻게 해야할까.
type Shape = 'SQUARE' | 'TRIANGLE' | 'CIRCLE' | string
간단하게 위와 같이 정의해볼 수 있을 것이다. 하지만 이는 잘못된 구현이다.
위와 같이 정의하면 유니온 타입의 특성상 `Shape` 은 문자열 리터럴 타입이 아니라 `string` 타입으로 확장되어 `string` 타입으로만 추론된다.
이는 TypeScript의 타입 시스템이 유니온 타입을 해석하는 방식에 기인한 동작으로, “일반" 타입(`string`)과 “리터럴" 타입(`SQUARE`, `TRIANGLE`, `CIRCLE`)이 결합되면 유니온의 모든 요소를 포함할 수 있는 상위(superset) 타입으로 해석한다.
(위 경우, `string`이 `superset`타입이 되므로, 문자열 리터럴을 구분하지않고 `string`으로 해석된다.)
Then How?
그렇다면 어떻게 하면 위 조건을 만족하는 타입을 정의할 수 있을까.
아래 타입을 한번 봐보자.
type Shape = 'SQUARE' | 'TRIANGLE' | 'CIRCLE' | (string & {})
이렇게 하면 (조건 1)과 (조건 2)를 만족하는 타입을 정의할 수 있다!
아까와는 달리 `(string & {})` 라는 생소한 표현식이 들어간 것을 알 수 있는데, 이것이 어떻게 동작하길래 조건을 만족하게 할 수 있는 것일까.
얼핏 보았을 때는 결국 `'SQUARE’ | ... | string` 으로 정의한 것과 별반 다르지 않아 보이지만, 타입스크립트의 타입 시스템은 이를 다르게 처리한다.
`'SQUARE’ | ... | string`은 리터럴 문자열과 함께 유니온되어 `string`타입으로 추론되었다면. `'SQUARE’ | ... | (string & {})`은 우선적으로 문자열 리터럴을 타입 힌트로 제공하고 어느 문자열 리터럴에도 속하지 않는다면, `string & {}` 타입으로 추론된다. 이는 곧 `string`으로 추론되어 문자열을 입력할 수 있도록 한다.
* `string & {}` 타입이 `string` 으로 추론되는 이유는 (`|`에 경우와 다르게) `&`에 경우 타입 결합 시 하위(subset) 타입으로 추론되기 때문에, `string`이 추론된다.
참고자료