유니언 타입은 |로 선택지를 나열합니다. string | number 타입의 값은 문자열이거나 숫자입니다. 유니언은 정당하게 두 가지 이상의 형태를 가질 수 있는 값을 TypeScript가 기술하는 방법이며, 컴파일러는 어떤 형태에만 있는 것을 쓰기 전에 지금 어떤 형태인지 확인하게 만듭니다.
typeof 검사의 각 분기 안에서 id는 타입이 하나입니다. 이 단계를 좁히기(narrowing)라고 하며, 이것이 유니언을 실용적으로 만듭니다.
공통 멤버만 허용된다
좁히기 전에는 유니언의 모든 멤버가 지원하는 것만 쓸 수 있습니다. toString()은 문자열과 숫자 모두에 있으므로 괜찮지만, toUpperCase()는 문자열에만 있습니다.
index.ts(3,16): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.
Property 'toUpperCase' does not exist on type 'number'.
두 번째 줄이 속성이 없는 멤버의 이름을 알려 줍니다. 반대 방향에도 같은 규칙이 적용됩니다. string | number 값은 숫자일 수도 있으므로 string 타입 매개변수에 넘길 수 없습니다(TS2345). 유니언은 더 많은 값을 받는 대신, 확인하기 전까지는 그 값으로 할 수 있는 일이 적습니다.
유니언 좁히기
좁히기에는 평범한 JavaScript 검사를 씁니다. TypeScript는 제어 흐름을 따라가며 배제된 멤버를 없애므로, 마지막 검사 뒤에는 멤버 하나만 남습니다.
| 검사 | 좁히는 대상 | 쓰기 좋은 곳 |
|---|---|---|
typeof x === "string" | 해당 원시 타입 | string, number, boolean, bigint, symbol, undefined, function |
x === null, x === "a" | 비교한 값 | null, undefined, 리터럴 멤버 |
Array.isArray(x) | 배열 멤버 | 배열 |
x instanceof Date | 해당 클래스 | 클래스 인스턴스 |
"meow" in x | 그 속성을 가진 멤버 | 객체 타입 |
x.kind === "circle" | 그 태그를 가진 멤버 | 판별 유니언 |
isCat(x)(x is Cat을 반환) | 함수가 말하는 타입 | 무엇이든, 직접 만든 로직 |
좁히기 형태의 전체 목록은 타입 좁히기 페이지에 있습니다.
리터럴 타입의 유니언
리터럴 값의 유니언은 허용되는 값의 닫힌 집합입니다. 실제 코드에서 가장 흔한 유니언입니다.
type Status = "idle" | "loading" | "success" | "error";
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
type Toggle = "on" | "off" | boolean; // boolean is itself true | false
let current: Status = "idle";
current = "loading"; // fine
current = "finished"; // error TS2322: Type '"finished"' is not assignable to type 'Status'.
리터럴과 비교하면 좁혀집니다. if (current === "error") 뒤의 else 분기는 current가 나머지 셋 중 하나라는 것을 압니다. 많은 코드베이스에서 리터럴 유니언이 enum을 대신합니다. as const와 배열에서 이런 유니언을 끌어내는 방법은 리터럴 타입을 보세요.
객체 타입의 유니언
멤버가 객체 타입이면, 모두가 공유하는 속성은 바로 쓸 수 있습니다. 나머지는 in으로 속성이 있는지 확인하세요.
객체 모양 여러 개의 유니언이라면 kind: "cat" / kind: "fish" 같은 공유 리터럴 속성을 두는 편이 더 깔끔합니다. 그 속성 하나를 확인하면 객체 전체가 좁혀지고, 그것에 대한 switch는 완전성 검사를 받을 수 있습니다. 이 패턴이 판별 유니언입니다.
배열과 유니언
괄호의 위치에 따라 의미가 완전히 달라집니다.
| 타입 | 의미 | 값의 예 |
|---|---|---|
(string | number)[] | 각 요소가 문자열이거나 숫자인 배열 | [1, "two", 3] |
string[] | number[] | 문자열만 담은 배열이거나 숫자만 담은 배열 | ["a", "b"] |
string | number[] | 문자열이거나 숫자 배열(|가 []보다 약하게 결합) | "text" |
(string | number)[]를 순회하면 각 요소가 유니언이므로, 위의 reduce 콜백처럼 좁혀야 합니다. map과 filter 같은 메서드는 string[] | number[]에서도 동작하며, 콜백은 string | number를 받습니다.
null, undefined와의 유니언
가장 흔한 유니언은 "값 또는 아무것도 없음"입니다: string | null, User | undefined. Array.prototype.find와 Map.prototype.get이 반환하는 것이 이것이고, 선택적 속성 name?: string은 읽으면 string | undefined입니다. ?., ??, null 검사로 이를 다루는 방법은 별도 페이지 null과 undefined에 있습니다.
타입 수준에서 기존 유니언의 멤버를 없애려면 내장 유틸리티를 쓰세요. Exclude<"a" | "b" | "c", "a">는 "b" | "c"이고, NonNullable<string | null>은 string입니다.
자주 묻는 질문
TypeScript에서 유니언 타입이란 무엇인가요?
여러 선택지를 |로 이은 타입입니다. string | number 타입의 값은 문자열이거나 숫자일 수 있습니다. typeof value === "string" 같은 검사로 값을 한 멤버로 좁히기 전까지, 컴파일러는 모든 멤버가 공통으로 가진 것만 쓰게 합니다.
TypeScript가 유니언 타입에 속성이 없다고 하는 이유는 무엇인가요?
유니언의 멤버 중 적어도 하나에 그 속성이 없기 때문입니다. 예를 들어 오류 TS2339 Property 'toUpperCase' does not exist on type 'string | number'는 값이 toUpperCase가 없는 숫자일 수도 있다는 뜻입니다. 먼저 좁히고(typeof, in, Array.isArray, instanceof, 또는 판별자 검사) 그다음 멤버 전용 속성을 쓰세요.
두 가지 이상의 타입을 담는 배열은 어떻게 선언하나요?
유니언을 괄호로 감싸세요: (string | number)[] 또는 Array<string | number>로, 각 요소가 둘 중 어느 타입이든 될 수 있습니다. string[] | number[]는 다릅니다. 배열 전체가 모두 문자열이거나 모두 숫자입니다. 괄호가 없으면 string | number[]는 문자열이거나 숫자 배열이라는 뜻입니다.
유니언 타입과 교차 타입의 차이는 무엇인가요?
유니언 A | B는 둘 중 하나인 값이므로 둘이 공유하는 것만 쓸 수 있습니다. 교차 A & B는 동시에 둘 다인 값이므로 양쪽의 모든 멤버를 갖습니다. 객체 타입이라면 A | B는 더 많은 값을 받고, A & B는 더 많은 속성을 요구합니다.
유니언 값이 어떤 타입인지 어떻게 확인하나요?
TypeScript가 이해하는 런타임 검사를 쓰세요. 원시 타입에는 typeof x === "string", 배열에는 Array.isArray(x), 클래스에는 x instanceof Date, 객체 모양에는 "prop" in x, 멤버들이 리터럴 태그를 공유한다면 x.kind === "circle"입니다. 직접 만든 로직이라면 x is T를 반환하는 타입 가드 함수를 작성하세요.