Menu

TypeScript 유니언 타입(|) 사용법과 예제

string | number 같은 유니언 타입은 값이 여러 타입 중 하나일 수 있다는 뜻입니다. 유니언으로 할 수 있는 것(모든 멤버가 지원하는 것만), 유니언을 좁히는 방법, 리터럴과 객체 타입의 유니언, (A | B)[]와 A[] | B[]의 차이를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

유니언 타입은 |로 선택지를 나열합니다. 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를 반환하는 타입 가드 함수를 작성하세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기