본문으로 건너뛰기

좁히기 — typeof와 타입 추적 표

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

목차

좁히기(narrowing)는 검사기가 typeof 같은 확인을 읽고, 확인한 갈래 안에서 이름의 타입을 더 작은 모음으로 줄여 보는 일입니다. 검사기는 값을 모르니 if·else 두 갈래를 모두 읽고, 길이 다시 만나는 줄에서는 그 줄로 오는 길들의 타입을 합칩니다.

유니언 — 여럿 가운데 하나에서 string | number 값에는 두 타입이 함께 가진 것만 쓸 수 있었습니다. 글자일 때만 toUpperCase()를 부르려면 먼저 글자인지 확인해야 하고, 검사기도 그 typeof 확인을 읽습니다. 이 레슨에서 검사 규칙 5의 앞 절반을 세우고, 줄마다의 지금 타입을 손으로 적는 타입 추적 표를 배웁니다.

8단계 중 1단계. 다음을 누르면 검사기가 읽는 줄을 하나씩 짚습니다. 표에는 그 줄에서 검사기가 보는 타입이 쌓입니다.

1typeof로 확인한 갈래 안에서 좁혀진다

JS 다시 보기: typeof는 값의 종류를 'number'·'string' 같은 글자로 돌려줍니다(JS '수·글자·불리언과 typeof').

매개변수 v에는 주문 번호가 키오스크의 수 7이나 앱의 글자 'a7'로 넘어옵니다. 수면 '7번'처럼, 글자면 대문자로 만듭니다.

function show(
  v: string | number,
) {
  if (typeof v === 'number') {
    return v + '번';
  }
  return v.toUpperCase();
}
console.log(show(7));
console.log(show('a7'));
실행 결과
7번
A7

유니언 레슨에서 막혔던 toUpperCase()가 7번 줄에서는 지나갑니다. 검사기는 이 코드를 이렇게 읽습니다.

  1. 2번 줄: v의 선언 타입은 string | number입니다. 함수 첫머리에서 v의 지금 타입도 같습니다.
  2. 4번 줄: typeof v === 'number'⁠를 읽고 길을 둘로 나눕니다. 조건이 참인 길(갈래 안으로 들어가는 길)에서는 v를 number로 좁혀 보고, 거짓인 길(갈래를 건너뛰는 길)에서는 number를 뺀 string으로 봅니다.
  3. 5번 줄: v는 number입니다. return으로 함수가 끝나므로 이 길은 아래로 오지 않습니다.
  4. 7번 줄: 이 줄로 오는 길은 4번 줄의 거짓 길 하나뿐입니다. 그래서 v는 string이고, toUpperCase()가 지나갑니다.

show(7)이 갈래 안으로 가는지는 실행 때의 일입니다. 검사기는 값을 모르니 두 길을 모두 읽고, 길마다 v의 타입을 따로 봅니다. v의 선언 타입은 끝까지 string | number이지만, 검사기가 그 줄에서 보는 지금 타입은 5번 줄에서 number, 7번 줄에서 string입니다. 선언 타입과 지금 타입이 여기서 처음 갈립니다.

2길이 만나는 줄에서 합쳐진다

이번에는 5번 줄에서 '7번'을 돌려주지 않고 console.log로 찍기만 합니다. 7번 줄에서 v의 지금 타입은 무엇일까요? 그 줄은 검사를 지나갈까요? 답을 정한 뒤 펼쳐 보세요.

function show(
  v: string | number,
) {
  if (typeof v === 'number') {
    console.log(v + '번');
  }
  return v.toUpperCase();
}
console.log(show(7));
console.log(show('a7'));
검사 결과와 실행 결과 보기
검사 결과 오류 1개
main.ts(7,12): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.  Property 'toUpperCase' does not exist on type 'number'.
실행 결과
7번main.ts:7  return v.toUpperCase();           ^ TypeError: v.toUpperCase is not a function    at show (main.ts:7:12)    at main.ts:9:13

예상과 다르면 그곳이 배울 곳입니다. 7번 줄에서 v의 지금 타입은 string | number이고, 검사는 7번 줄에서 막힙니다. 이번에는 5번 줄의 갈래가 return 없이 끝나, 갈래 안을 지난 길도 6번 줄을 지나 7번 줄로 옵니다. 7번 줄에는 두 길이 만납니다. 갈래 안을 지나온 참 길은 number를, 4번 줄의 거짓 길은 string을 가져옵니다. 검사기는 만나는 줄에서 두 길의 타입을 합쳐 string | number로 봅니다. 그 가운데 number에는 toUpperCase가 없습니다.

Node로 돌리면 show(7)이 5번 줄에서 7번을 찍은 뒤 7번 줄까지 내려와 멈춥니다. 검사기가 막은 길이 실제로 돈 것입니다.

1절과 2절은 5번 줄 하나만 다릅니다 — 1절은 갈래가 return으로 끝나 7번 줄에 길 하나만 왔고, 2절은 두 길이 다 왔습니다. 만나는 줄의 타입은 선언 타입으로 돌아가는 것도, 갈래 안의 타입이 이어지는 것도 아니고, 그 줄로 오는 길들이 가져온 타입을 합친 것입니다.

검사 규칙 5. 검사기는 값을 모릅니다. 그래서 갈 수 있는 길을 모두 읽습니다 — if·else는 두 갈래를 모두 읽습니다. typeof 같은 확인 뒤의 갈래에서는 이름의 지금 타입이 좁혀지고, 길이 다시 만나는 줄에서는 그 줄로 오는 길들의 지금 타입이 합쳐집니다. return으로 끝난 길은 오지 않습니다.

뒤 절반(대입과 반복)은 좁힘이 달라지는 곳 — 대입과 반복에서 이어 봅니다.

3타입 추적 표로 줄마다 적기

타입 추적 표(type trace table)는 검사기가 줄마다 보는 이름의 지금 타입을 차례로 적는 표입니다. 칸은 셋입니다 — 줄, 이름, 지금 타입. 쓰는 규칙도 셋입니다.

  • 이름을 읽는 줄마다 한 줄을 씁니다. 실행 차례가 아니라 코드 차례로, 갈래 안의 줄도 건너뛰지 않습니다. 반복 머리 행만은 덩어리 끝을 먼저 적고 채웁니다(다음 레슨).
  • 확인하는 줄(if 줄)에는 확인하기 전 타입을 쓰고, 조건이 참일 때와 거짓일 때 길이 가져가는 타입을 나눠 씁니다.
  • 길이 만나는 줄에는 그 줄로 오는 길마다 타입을 쓰고 합칩니다. return으로 끝난 길은 '오지 않음'이라고 씁니다.

2절 코드를 이 표로 적으면 이렇습니다.

줄이름지금 타입
2vstring | number
4vstring | number → 참 number 거짓 string
5vnumber
7v만나는 줄 — 참 number + 거짓 string → string | number

1절 코드는 5 · 7번 줄 행만 다릅니다.

줄이름지금 타입
5vnumber — return으로 끝남
7v만나는 줄 — 참은 오지 않음 + 거짓 string → string

표는 끝까지 씁니다. 만나는 줄 행을 빼고 머릿속으로 넘기면, 7번 줄의 타입을 갈래 안의 타입이나 선언 타입으로 짐작하게 됩니다. 반만 쓴 표는 안 쓴 것과 같습니다. 편집기에서 이름에 마우스를 올리면 보이는 타입이 이 표의 지금 타입이라, 표를 쓴 뒤 맞춰 볼 수 있습니다.

예제

확인 둘로 셋을 가르기

영수증의 한 칸에는 메뉴 이름(글자), 금액(수), 포장 여부(참·거짓)가 들어갑니다. typeof 확인 둘로 셋을 가릅니다.

function cell(
  v: string | number | boolean,
) {
  if (typeof v === 'string') {
    return v;
  }
  if (typeof v === 'number') {
    return v + '원';
  }
  return v ? '포장' : '매장';
}
console.log(cell('라테'));
console.log(cell(4500));
console.log(cell(true));
실행 결과
라테
4500원
포장

5번 줄과 8번 줄의 갈래는 모두 return으로 끝납니다. 10번 줄의 v ? '포장' : '매장'은 참·거짓 값으로 둘 가운데 하나를 고릅니다. 줄마다의 지금 타입은 확인 문제 2에서 타입 추적 표로 채웁니다.

자주 틀리는 곳

틀린 생각: typeof로 확인한 if 아래 줄에서는 그 이름이 늘 다른 쪽 타입이다. 또는, 갈래를 나오면 선언 타입으로 돌아간다.

4번 줄에서 number인 경우를 다뤘으니 아래 줄에는 글자만 온다고 여기거나, 좁힌 타입은 갈래 안에서만 산다고 여기기 쉽습니다. 그러나 7번 줄의 v는 1절에서 string, 2절에서 string | number였습니다 — 1절은 5번 줄의 참 길이 return으로 끝나 거짓 길 하나만 왔고, 2절은 두 길이 다 왔습니다.

맞는 규칙: 만나는 줄의 지금 타입은 선언 타입이 아니라 그 줄로 오는 길들이 가져온 타입을 합친 것이고, return으로 끝난 길은 오지 않습니다(검사 규칙 5).

틀린 생각: 검사기는 실제로 넘어오는 값의 갈래만 읽는다.

함수를 글자로만 부르면 수 갈래는 돌 일이 없으니, 검사기도 그 길은 보지 않을 것 같습니다. 2절 코드에서 show(7)을 지우고 show('a7')만 남겼습니다.

function show(
  v: string | number,
) {
  if (typeof v === 'number') {
    console.log(v + '번');
  }
  return v.toUpperCase();
}
console.log(show('a7'));
검사 결과 오류 1개
main.ts(7,12): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.  Property 'toUpperCase' does not exist on type 'number'.
실행 결과
A7

실행은 A7을 찍고 끝나지만, 검사는 그대로 7번 줄에서 막힙니다.

맞는 규칙: 검사기는 값을 모르니 매개변수의 타입으로 올 수 있는 길을 모두 읽습니다. 부르는 줄의 값을 보고 길을 고르지 않습니다(검사 규칙 5).

틀린 생각: typeof v === 'object'⁠로 확인하면 v는 null이 아니다.

'object'를 확인하면 확인한 값은 늘 무엇이 든 객체라고 여기기 쉽습니다.

JS 다시 보기: typeof null은 'object'입니다(JS '수·글자·불리언과 typeof').

function size(
  v: string[] | null,
) {
  if (typeof v === 'object') {
    return v.length;
  }
  return 0;
}
검사 결과 오류 1개
main.ts(5,12): error TS18047: 'v' is possibly 'null'.

'v' is possibly 'null'⁠은 'v가 null일 수 있다'는 뜻입니다. 검사기도 typeof null이 'object'인 것을 알아, 4번 줄의 갈래 안에서 v를 string[] | null로 봅니다.

맞는 규칙: typeof가 'object'인 값에는 null도 들어 있습니다. null을 거르려면 v !== null처럼 따로 확인합니다(검사 규칙 5).

확인 문제

1. 예제 코드에서 8번 줄의 return을 지워 v + '원';만 남겼습니다. 10번 줄에서 v의 지금 타입을 쓰세요.

function cell(
  v: string | number | boolean,
) {
  if (typeof v === 'string') {
    return v;
  }
  if (typeof v === 'number') {
    v + '원';
  }
  return v ? '포장' : '매장';
}
힌트

10번 줄로 오는 길을 셉니다. 8번 줄의 갈래가 이제 어떻게 끝나는지 봅니다.

정답
검사 결과 오류 없음

number | boolean입니다(차례가 바뀐 boolean | number도 같은 답). 검사 결과는 '오류 없음'입니다 — 참·거짓으로 고르는 10번 줄은 수에도 쓸 수 있어서 막히지 않습니다. 7번 줄의 갈래가 이제 return 없이 끝나, 10번 줄에는 갈래 안을 지난 참 길(number)과 7번 줄의 거짓 길(boolean)이 함께 옵니다. 둘을 합쳐 number | boolean입니다. 5번 줄 갈래는 그대로 return으로 끝나서, string은 오지 않습니다.

흔한 오답은 둘입니다. boolean은 고치기 전 코드의 답으로, 8번 줄의 return이 있어야 number 길이 오지 않습니다. string | number | boolean⁠은 5번 줄 길도 온다고 본 것입니다(갈래를 나오면 선언 타입이라는 생각).

2. 예제 코드(cell)를 타입 추적 표로 적었습니다. 표의 (가)~(라)를 채우세요.

줄이름지금 타입
2vstring | number | boolean
4vstring | number | boolean → 참 string 거짓 (가)
5vstring — return으로 끝남
7v만나는 줄 — 참은 오지 않음 + 거짓 → (나)
7v(나) → 참 number 거짓 (다)
8vnumber — return으로 끝남
10v만나는 줄 — 참은 오지 않음 + 거짓 → (라)
힌트

거짓 길의 타입은 확인하기 전 타입에서 확인한 타입을 뺀 것입니다. 만나는 줄에서는 '오지 않음'인 길을 빼고 합칩니다.

정답

(가) number | boolean · (나) number | boolean · (다) boolean · (라) boolean

(가)는 string | number | boolean⁠에서 string을 뺀 것입니다. 5번 줄의 참 길이 return으로 끝나 7번 줄에는 거짓 길 하나만 오니 (나)도 같습니다. (다)는 거기서 number를 뺀 boolean, (라)는 8번 줄의 참 길이 오지 않아 boolean 하나입니다.

7번 줄은 행이 둘입니다 — 먼저 길이 만나고, 그다음 같은 줄에서 다시 확인해 길이 갈립니다. 흔히 빠뜨리는 것이 (나)입니다. 만나는 줄 행을 빼면 7번 줄의 v를 선언 타입 그대로 셋으로 적기 쉽습니다.

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

function mark(
  v: number | string,
) {
  if (typeof v === 'string') {
    return v;
  }
  return v.toFixed(1);
}
console.log(mark(90));
console.log(mark('결석'));
  • ① 검사를 지나고, 실행하면 90.0과 결석이 찍힌다.
  • ② 검사는 7번 줄에서 막히고, 실행하면 90.0과 결석이 찍힌다.
  • ③ 검사를 지나지만, 실행하면 mark('결석')에서 멈춘다.
힌트

타입 추적 표로 4번 · 5번 · 7번 줄을 적어 봅니다. 실행은 값마다 어느 줄을 지나는지 따라갑니다.

정답
검사 결과 오류 없음
실행 결과
90.0
결석

①입니다. 5번 줄 갈래가 return으로 끝나, 7번 줄에는 string을 뺀 number 길만 옵니다. toFixed()는 수에 있으니 검사를 지납니다. 실행하면 90은 7번 줄에서 90.0이 되고, '결석'은 5번 줄에서 그대로 돌아옵니다.

  • ② 갈래를 나오면 v가 선언 타입 number | string으로 돌아간다고 본 것입니다(갈래를 나오면 선언 타입이라는 생각). 만나는 줄에는 오는 길의 타입만 합칩니다.
  • ③ '결석'도 7번 줄까지 내려간다고 본 것입니다. return을 만나면 함수가 끝나 아래 줄로 가지 않습니다.

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 1절 코드를 main.ts로 저장하고 npx tsc로 검사합니다. 5번 줄의 return을 지우면(v + '번';만 남김) 7번 줄 v의 지금 타입과 검사 결과가 어떻게 될지 먼저 적고, 바꿔서 맞춰 봅니다. 편집기에서 4번 · 5번 · 7번 줄의 v에 마우스를 올려 지금 타입을 견줘 봅니다. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

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

타입스크립트 목차로