본문으로 건너뛰기

유니언 — 여럿 가운데 하나

타입스크립트 목차유니언과 좁히기레슨 19 / 51

목차

유니언(union) 타입 string | number는 string 모음과 number 모음을 합친 타입입니다. 두 모음 가운데 어느 쪽 값이든 넣을 수 있지만, 그 값에는 두 타입이 함께 가진 것만 쓸 수 있습니다.

카페의 주문 번호는 키오스크에서는 수 7로, 앱에서는 글자 'a7'로 들어옵니다. 매개변수 타입을 string 하나로 적으면 수 번호를 넘기는 줄이 막히고, number 하나로 적으면 글자 번호를 넘기는 줄이 막힙니다. 자바스크립트로 쓴 코드에는 이렇게 '둘 가운데 하나'인 값이 흔합니다. 유니언은 그런 값의 타입을 적는 꼴입니다.

이 단원을 이미 안다면 단원 연습 문제의 진단 문항부터 풀고 틀린 레슨만 보세요.

10단계 중 1단계. 다음을 누르면 검사기가 읽는 줄을 하나씩 짚습니다.

1두 모음을 합친 타입

주문 번호를 받아 '주문 a7'처럼 만드는 함수입니다. 매개변수 id의 타입을 string으로 적었더니 수 번호를 넘기는 7번 줄이 막힙니다.

function show(
  id: string,
) {
  return '주문 ' + id;
}
console.log(show('a7'));
console.log(show(7));
검사 결과 오류 1개
main.ts(7,18): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

이 TS2345도 string 자리에 수를 넘겼다는 뜻입니다.

매개변수 타입을 string | number로 적으면 두 줄 모두 지나갑니다.

function show(
  id: string | number,
) {
  return '주문 ' + id;
}
console.log(show('a7'));
console.log(show(7));
실행 결과
주문 a7
주문 7

| 기호는 두 타입의 모음을 합칩니다. string | number는 글자 값과 수 값을 모두 담은 모음 하나이고, 이것이 매개변수 id의 선언 타입입니다. 인자를 넘길 때마다 검사기는 인자의 타입이 이 모음에 드는지 봅니다(검사 규칙 2). 'a7'은 string 쪽에, 7은 number 쪽에 듭니다.

두 모음 어디에도 들지 않는 값은 그대로 막힙니다. 8번 줄에 console.log(show(true));⁠를 더하면 TS2345가 나고, 오류 문장 끝의 받는 자리 타입은 'string | number'입니다. boolean 값은 합친 모음에도 없습니다.

2함께 가진 것만 쓸 수 있다

앱 번호 'a7'을 대문자 A7로 찍으려고 4번 줄에서 toUpperCase()를 불렀습니다.

JS 다시 보기: toUpperCase()는 문자열의 메서드입니다(JS '문자열 메서드').

이 코드는 검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? 답을 정한 뒤 펼쳐 보세요.

function show(
  id: string | number,
) {
  return id.toUpperCase();
}
console.log(show('a7'));
console.log(show(7));
검사 결과와 실행 결과 보기
검사 결과 오류 1개
main.ts(4,13): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.  Property 'toUpperCase' does not exist on type 'number'.

예상과 다르면 그곳이 배울 곳입니다. 4번 줄에서 막힙니다. 첫 줄은 'string | number 타입에는 toUpperCase 속성이 없다(does not exist)', 곧 수일 수도 있는 값에 문자열 메서드를 불렀다는 뜻입니다. 들여 쓴 둘째 줄이 까닭입니다 — 합친 모음 가운데 number 쪽에 그 속성이 없습니다.

검사기는 값을 모릅니다. 함수 안을 읽을 때 id에 무엇이 넘어올지는 매개변수 타입 string | number로만 압니다. 실제로도 6번 줄은 글자를, 7번 줄은 수를 넘깁니다. 그래서 검사기는 두 타입이 함께 가진 것만 쓰게 합니다.

Node로 돌리면 검사기가 막은 바로 그곳에서 멈춥니다.

실행 결과
A7main.ts:4  return id.toUpperCase();            ^ TypeError: id.toUpperCase is not a function    at show (main.ts:4:13)    at main.ts:7:13

'a7'은 A7로 찍히고, 7을 넘긴 둘째 부르기에서 멈춥니다. 검사기는 이 길을 실행하기 전에 찾아 준 것입니다.

함수 안에서 검사기가 보는 id의 지금 타입은 선언 타입과 같은 string | number입니다. 그래서 toUpperCase()처럼 string 쪽에만 있는 메서드도, toFixed()처럼 number 쪽에만 있는 메서드도 막히고, toString()처럼 둘 다 가진 메서드만 지나갑니다.

한쪽에만 있는 것을 쓰려면 먼저 어느 쪽인지 확인해야 합니다. 확인해서 타입을 좁히는 법은 좁히기 — typeof와 타입 추적 표에서 배웁니다.

3없을 수 있는 값 — string | null

유니언에서 가장 자주 만나는 꼴은 null이나 undefined를 더한 타입입니다. 메모가 없을 수 있으면 memo: string | null처럼 적습니다. 원시 타입과 null·undefined에서 본 대로 null은 string 모음 밖의 값이라, 없을 수 있다는 것을 타입에 적어 둬야 그 자리에 null이 들어갑니다.

function size(
  memo: string | null,
) {
  return memo.length;
}
검사 결과 오류 1개
main.ts(4,10): error TS18047: 'memo' is possibly 'null'.

'memo' is possibly 'null'⁠은 'memo가 null일 수 있다'는 뜻입니다. 2절과 같은 까닭입니다 — null 쪽 모음에는 속성이 하나도 없어서, 두 타입이 함께 가진 속성도 없습니다. null이 섞인 유니언에서는 검사기가 이렇게 따로 알려 줍니다. null·undefined를 거른 뒤에 쓰는 법은 null·undefined 거르기에서 배웁니다.

예제

섞인 배열에서 꺼낸 값

배열과 튜플에서 ['라테', 4500]의 타입이 (string | number)[]인 것을 봤습니다. 이 타입도 유니언을 쓴 것입니다 — 원소 하나하나가 string이거나 number입니다.

const row = ['라테', 4500];
const first = row[0];
console.log(first.length);
검사 결과 오류 1개
main.ts(3,19): error TS2339: Property 'length' does not exist on type 'string | number'.  Property 'length' does not exist on type 'number'.
실행 결과
2

실행하면 row[0]이 '라테'라서 2가 찍힙니다. 그래도 검사는 막힙니다. 검사기는 배열의 몇 번째에 무엇이 있는지 모르고, 꺼낸 값의 타입을 원소 타입 string | number로 봅니다. 검사 결과와 실행 결과는 따로입니다 — 검사에서 막혀도 Node는 돌립니다.

자리마다 타입이 정해진 배열이면 튜플로 적습니다. row[0]이 string으로 보여 검사를 지납니다.

const row: [string, number] =
  ['라테', 4500];
const first = row[0];
console.log(first.length);
실행 결과
2

자주 틀리는 곳

틀린 생각: 유니언 값에는 두 타입 가운데 아무 쪽의 메서드나 쓸 수 있다.

string | number는 '글자도 되고 수도 된다'로 읽혀, 글자 메서드도 수 메서드도 다 열린 것처럼 보입니다. 검사기는 거꾸로 봅니다 — 둘 가운데 무엇이 올지 모르니, 둘 다에 있는 것만 쓰게 합니다.

2절 코드의 4번 줄 id.toUpperCase()는 'a7'이 넘어올 때는 돌지만, 7이 넘어오면 실행 중 TypeError로 멈췄습니다. 검사기는 그 길을 실행하기 전에 막았습니다.

맞는 규칙: 유니언 값은 넘어올 수 있는 값 모두에 되는 것만 쓸 수 있습니다. 한쪽에만 있는 것을 쓰려면 먼저 어느 쪽인지 확인해 좁힙니다(검사 규칙 2).

틀린 생각: 오류의 들여 쓴 둘째 줄은 첫 줄을 한 번 더 말한 것이다.

두 줄이 거의 같은 낱말이라 둘째 줄은 건너뛰기 쉽습니다. 다른 곳은 끝의 타입 하나입니다.

2절 오류의 첫 줄 끝은 'string | number', 둘째 줄 끝은 'number'입니다. 첫 줄은 '합친 타입에 없다'고 말하고, 둘째 줄은 그 가운데 어느 쪽에 없는지 짚습니다. 4번 줄을 고칠 때 볼 것은 number 값이 넘어오는 길입니다.

맞는 규칙: 들여 쓴 둘째 줄은 유니언의 어느 쪽 때문에 막혔는지 알려 줍니다. 그쪽 값이 넘어오는 길을 찾아 고칩니다.

틀린 생각: 오류 문장의 유니언은 내가 적은 차례대로 찍힌다.

number | string으로 적었는데 오류가 string | number라고 하면, 다른 타입을 말하는 것처럼 보입니다.

function show(
  id: number | string,
) {
  return id.toUpperCase();
}
검사 결과 오류 1개
main.ts(4,13): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.  Property 'toUpperCase' does not exist on type 'number'.

유니언은 모음을 합친 것이라 차례가 없습니다. number | string과 string | number는 같은 타입이고, 검사기는 제 차례로 찍습니다.

맞는 규칙: 유니언은 차례가 없는 모음이라, 적은 차례와 찍힌 차례가 달라도 같은 타입입니다. 오류 문장을 읽을 때는 차례가 아니라 든 타입을 맞춰 봅니다.

확인 문제

1. 2절 코드의 4번 줄을 return id.toFixed(0);⁠으로 바꿨습니다. 검사 결과의 들여 쓴 둘째 줄은 어느 타입을 말할까요?

function show(
  id: string | number,
) {
  return id.toFixed(0);
}
  • ① 'string'
  • ② 'number'
  • ③ 'string | number'
힌트

toFixed()가 어느 쪽 값에만 있는 메서드인지 봅니다.

정답
검사 결과 오류 1개
main.ts(4,13): error TS2339: Property 'toFixed' does not exist on type 'string | number'.  Property 'toFixed' does not exist on type 'string'.

①입니다. toFixed()는 수에만 있는 메서드라, 합친 모음 가운데 string 쪽에 없습니다. 둘째 줄은 그쪽을 짚습니다.

  • ② 2절 오류의 둘째 줄을 그대로 옮겨 본 것입니다. 둘째 줄의 타입은 그 메서드가 없는 쪽이라, 부른 메서드에 따라 달라집니다.
  • ③ 첫 줄 끝의 타입입니다. 둘째 줄은 그 가운데 어느 쪽인지를 짚습니다.

2. 테이블 번호는 수 3이거나 글자 '창가'입니다. 6~7번 줄이 모두 검사를 지나도록 빈칸에 타입을 쓰세요.

function table(
  no: ________,
) {
  return '테이블 ' + no;
}
console.log(table(3));
console.log(table('창가'));
힌트

6번 줄과 7번 줄이 넘기는 값의 타입을 하나씩 적어 봅니다.

정답

number | string입니다.

function table(
  no: number | string,
) {
  return '테이블 ' + no;
}
console.log(table(3));
console.log(table('창가'));
실행 결과
테이블 3
테이블 창가

다르게 써도 되는 곳: string | number로 차례를 바꿔 써도 같은 타입입니다. number | '창가'처럼 값을 적어도 두 줄은 지나가지만, 그러면 '창가' 밖의 글자는 막힙니다 — 값으로 된 타입은 다음 레슨 리터럴 유니언으로 값 고르기에서 배웁니다.

흔한 오답은 number나 string 하나만 쓰는 것입니다. 그러면 다른 쪽 값을 넘기는 줄이 TS2345로 막힙니다.

3. 시험 점수를 담은 배열입니다. 결석한 사람은 점수 대신 '결석'이 들어 있습니다. 이 코드의 검사 결과와 실행 결과로 맞는 것을 고르세요.

const marks = [90, '결석', 75];
const first = marks[0];
console.log(first.toFixed(1));
  • ① 검사는 3번 줄에서 막히고, 실행하면 90.0이 찍힌다.
  • ② 검사는 3번 줄에서 막히고, 실행되지 않는다.
  • ③ 검사를 지나고, 실행하면 90.0이 찍힌다.
힌트

marks의 원소 타입을 먼저 적어 봅니다. 검사 결과와 실행 결과는 따로 따집니다.

정답
검사 결과 오류 1개
main.ts(3,19): error TS2339: Property 'toFixed' does not exist on type 'string | number'.  Property 'toFixed' does not exist on type 'string'.
실행 결과
90.0

①입니다. marks의 원소 타입은 string | number라서, 꺼낸 first도 string | number입니다. toFixed()는 string 쪽에 없어 3번 줄이 막힙니다. 실행하면 marks[0]이 수 90이라 90.0이 찍힙니다.

  • ② 검사에서 막히면 실행도 안 된다고 본 것입니다(검사 오류가 실행을 막는다는 생각). Node는 검사하지 않고 타입을 지운 코드를 그대로 돌립니다.
  • ③ 첫 원소가 90이니 first는 number라고 본 것입니다. 검사기는 배열의 몇 번째에 무엇이 있는지 모르고 원소 타입으로 봅니다.

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 2절 코드를 main.ts로 저장하고 npx tsc로 검사합니다. 4번 줄의 toUpperCase()를 toString()으로 바꾸면 검사 결과와 node main.ts의 출력이 어떻게 될지 먼저 적고, 바꿔 돌려서 맞춰 봅니다. 편집기에서 id에 마우스를 올리면 지금 타입도 보입니다. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

타입스크립트 목차로