본문으로 건너뛰기

null·undefined 거르기

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

목차

null·undefined 거르기는 없을 수도 있는 값을 쓰기 전에 있는지 확인하는 일입니다. 확인한 갈래 안에서 검사기는 그 이름의 타입에서 null·undefined를 빼고 봅니다.

선택 매개변수 memo?: string은 함수 안에서 string | undefined라, 그대로 memo.length를 쓰면 TS18048로 막힙니다(선택·기본값 매개변수). 이 레슨에서는 if로 먼저 거르는 길과, 거른 뒤 검사기가 보는 타입을 봅니다.

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

1참·거짓 값으로 거르기

메모가 있으면 '라테 — 얼음 적게'처럼, 없으면 '라테 — 메모 없음'을 찍는 영수증 한 줄입니다.

function line(memo?: string) {
  if (memo) {
    return '라테 — ' + memo;
  }
  return '라테 — 메모 없음';
}
console.log(line('얼음 적게'));
console.log(line());
실행 결과
라테 — 얼음 적게
라테 — 메모 없음

JS 다시 보기: if (memo)는 memo가 undefined나 빈 문자열 '' 같은 거짓 같은 값이면 갈래로 들어가지 않습니다(JS '참 같은 값과 ??').

검사기는 2번 줄의 if (memo)도 확인으로 읽습니다. undefined는 거짓 같은 값이라 조건이 참인 길(갈래 안으로 들어오는 길)에는 오지 않습니다. 그래서 3번 줄의 memo는 string으로 좁혀집니다.

줄이름지금 타입
1memostring | undefined
2memostring | undefined → 참 string 거짓 string | undefined
3memostring — return으로 끝남

거짓 길에 string이 남아 있는 것은 빈 문자열 '' 때문입니다. ''도 거짓 같은 값이라 거짓 길로 가므로, 검사기는 그 길의 memo를 string | undefined로 봅니다.

거르지 않으면 어떻게 될까요? +는 한쪽이 글자면 무엇이든 이어 붙이므로, '라테 — ' + memo는 거르지 않아도 검사를 지납니다.

function line(memo?: string) {
  return '라테 — ' + memo;
}
console.log(line());
실행 결과
라테 — undefined

검사 오류는 없지만 영수증에 undefined가 찍혔습니다. 검사기는 타입이 맞는지만 볼 뿐, 찍히는 글이 뜻대로인지는 모릅니다. 처음 코드의 if (memo)는 검사기 때문이 아니라 이런 결과를 막으려고 둔 확인입니다.

2!==로 견주어 거르기

이번에는 memo를 undefined와 바로 견주어 거릅니다.

function line(memo?: string) {
  if (memo !== undefined) {
    return '라테 — ' + memo;
  }
  return '라테 — 메모 없음';
}
console.log(line('얼음 적게'));
console.log(line());
실행 결과
라테 — 얼음 적게
라테 — 메모 없음

3번 줄의 memo는 1절과 같은 string입니다. 다른 것은 거짓 길입니다 — !== undefined는 undefined만 거짓 길로 보내고, 빈 문자열 ''은 참 길로 들입니다.

확인거짓 길
if (memo)string | undefined
if (memo !== undefined)undefined

그래서 빈 문자열을 넘기는 line('')은 if (memo)로 거르면 '라테 — 메모 없음'을 찍고, !== undefined로 거르면 '라테 — '만 찍습니다. 빈 메모를 '메모 없음'으로 볼지, 손님이 남긴 빈 문자열로 볼지에 따라 둘 가운데 고릅니다. 검사기는 어느 쪽이든 갈래 안의 memo를 string으로 봅니다. 타입이 string | null이면 memo !== null로 견주어 거릅니다. 갈래 안에서 null이 빠집니다.

3?.와 ??가 남기는 타입

JS 다시 보기: ?.는 왼쪽이 null·undefined이면 더 꺼내지 않고 undefined를 줍니다(JS '객체를 담은 배열'). JS 다시 보기: ??는 왼쪽이 null·undefined일 때만 오른쪽을 씁니다(JS '참 같은 값과 ??').

if 없이 한 줄로 거르는 두 기호입니다. 거른 결과에 붙는 타입은 서로 다릅니다. 메모를 비운 7번 줄 show()는 무엇을 찍을까요? 답을 정한 뒤 펼쳐 보세요.

function show(memo?: string) {
  const n = memo?.length;
  const text = memo ?? '없음';
  console.log(n, text);
}
show('얼음 적게');
show();
지금 타입과 실행 결과 보기
실행 결과
5 얼음 적게
undefined 없음

예상과 다르면 그곳이 배울 곳입니다. n의 지금 타입은 number | undefined, text는 string입니다. memo?.length는 memo가 있으면 글자 수(number)를, 없으면 length를 꺼내지 않고 undefined를 줍니다. 그래서 검사기는 결과 타입에 undefined를 더합니다. 7번 줄 show()에서 n이 실제로 undefined로 찍혔습니다. memo ?? '없음'은 memo가 없으면 '없음'을 씁니다. 어느 쪽이든 글자가 나오니 검사기는 undefined를 빼고 string으로 봅니다.

?. 기호는 없을 수 있다는 것을 결과에 그대로 남기고, ?? 기호는 없을 때 쓸 값을 정해 없앱니다. 남은 undefined는 다음 줄에서 다시 거르거나 ??로 정합니다.

예제

메모가 없을 수 있는 영수증 한 줄

메뉴 이름과 메모를 받아 영수증 한 줄을 만듭니다. 메모는 없을 수도 있고, 손님이 빈 문자열을 남길 수도 있습니다.

function line(
  item: string,
  memo?: string,
) {
  if (memo) {
    return item + ' — ' + memo;
  }
  return item + ' — 메모 없음';
}
const ice = '얼음 적게';
console.log(line('라테', ice));
console.log(line('라테'));
console.log(line('라테', ''));
실행 결과
라테 — 얼음 적게
라테 — 메모 없음
라테 — 메모 없음

6번 줄의 memo는 갈래 안이라 string입니다. 마지막 부르기의 빈 문자열 ''은 5번 줄의 if (memo)에서 걸러져 '메모 없음'이 찍힙니다.

자주 틀리는 곳

틀린 생각: 선택 매개변수로 받은 값은 늘 있다.

부르는 곳마다 메모를 넘기고 있으면, 함수 안의 memo도 늘 글자라고 여기기 쉽습니다. 검사기는 부르는 곳을 보지 않고 매개변수의 타입 string | undefined를 봅니다. memo.length는 TS18048(undefined일 수 있다)로 막히고, 실행하면 인자를 비운 부르기에서 TypeError로 멈춥니다.

코드와 검사 결과 보기
function size(
  memo?: string,
) {
  return memo.length;
}
console.log(size('얼음 적게'));
console.log(size());
검사 결과 오류 1개
main.ts(4,10): error TS18048: 'memo' is possibly 'undefined'.

맞는 규칙: 선택 매개변수와 선택 속성의 타입에는 undefined가 들어 있습니다. 쓰기 전에 if로 거르거나 ??로 없을 때의 값을 정합니다(검사 규칙 5).

틀린 생각: if (memo)와 if (memo !== undefined)⁠는 같은 값을 거른다.

둘 다 '메모가 있으면'으로 읽히고 갈래 안의 타입도 둘 다 string이지만, 2절 표처럼 거짓 길이 다릅니다.

맞는 규칙: 두 확인 모두 갈래 안의 이름을 string으로 좁히지만, 빈 문자열 ''을 어느 길로 보내는지가 다릅니다. 빈 문자열을 어떻게 다룰지 정해서 고릅니다(검사 규칙 5).

틀린 생각: memo?.length의 타입은 length의 타입 그대로 number다.

?. 기호도 거르는 것이라, 거른 뒤에는 없는 경우가 사라진다고 여기기 쉽습니다. 3절의 n은 number | undefined라서 n + 1이 TS18048로 막힙니다.

맞는 규칙: ?.의 결과 타입에는 undefined가 더해집니다. 수로 쓰려면 한 번 더 거르거나 ??로 정합니다(검사 규칙 5).

틀린 생각: memo!의 !가 memo가 있는지 확인해 준다.

검색하다 보면 memo!.length처럼 이름 뒤에 !를 붙여 TS18048을 없애는 코드를 만납니다. 오류가 사라지니 확인을 한 것처럼 보입니다.

function size(memo?: string) {
  const text = memo!;
  return text.length;
}
console.log(size());
검사 결과 오류 없음
실행 결과
main.ts:3  return text.length;              ^ TypeError: Cannot read properties of undefined (reading 'length')    at size (main.ts:3:15)    at main.ts:5:13

검사 결과는 오류 없음인데 실행은 3번 줄에서 멈춥니다. 2번 줄의 !는 검사기에게 'memo는 null·undefined가 아니다'라고 말하는 표시로, null 아님 단언(non-null assertion)이라고 합니다. 검사기는 그 말을 믿어 text를 string으로 보지만, !는 실행 전에 지워지고 값은 그대로 undefined입니다. 이런 단언이 왜 위험한지는 6단원 'as는 검사기에게 하는 말'에서 모아 봅니다.

맞는 규칙: !는 확인이 아니라 검사기에게 하는 말이라 실행 때 남지 않습니다. 없을 수 있는 값은 if나 ??로 실제로 확인합니다(검사 규칙 1).

확인 문제

1. 3절 코드의 2번 줄을 const n = memo?.length ?? 0;⁠으로 바꿨습니다. 2번 줄 n의 지금 타입은 무엇일까요?

function show(memo?: string) {
  const n = memo?.length ?? 0;
  console.log(n);
}
show('얼음 적게');
show();
  • ① number
  • ② number | undefined
  • ③ string | number
힌트

memo?.length의 타입을 먼저 적고, ??가 그 가운데 무엇을 바꾸는지 봅니다.

정답
실행 결과
5
0

①입니다. memo?.length는 number | undefined이고, ??가 undefined일 때 0을 쓰니 어느 쪽이든 수가 나옵니다. 그래서 n은 number입니다. 메모가 없는 둘째 부르기가 0을 찍습니다.

  • ② ??를 지나도 undefined가 남는다고 본 것입니다. ??는 없을 때 쓸 값을 정해 undefined를 없앱니다.
  • ③ 3절의 text처럼 글자가 섞인다고 본 것입니다. 오른쪽 값 0이 수라서 수만 나옵니다.

2. 3번 줄이 검사를 지나도록 2번 줄의 빈칸에 조건을 쓰세요.

function len(memo?: string) {
  if (________) {
    return memo.length;
  }
  return 0;
}
console.log(len('얼음 적게'));
console.log(len());
힌트

3번 줄로 들어오는 길에서 undefined를 빼는 확인을 1절 · 2절에서 찾습니다.

정답

memo입니다.

function len(memo?: string) {
  if (memo) {
    return memo.length;
  }
  return 0;
}
console.log(len('얼음 적게'));
console.log(len());
실행 결과
5
0

다르게 써도 되는 곳: memo !== undefined나 typeof memo === 'string'⁠도 3번 줄의 memo를 string으로 좁혀 검사를 지납니다. 이 함수는 셋 모두 len('')이 0입니다 — if (memo)는 ''을 건너뛰어 5번 줄의 0을, 나머지 둘은 ''.length인 0을 돌려줍니다.

흔한 오답은 빈칸 없이 if를 지우는 것입니다. 그러면 2번 줄의 확인이 없어 3번 줄에서 TS18048로 막힙니다.

3. 점수에 보너스 5점을 더하는 함수입니다. 점수가 없으면 보너스만 받습니다. 4번 줄이 TS18048로 막힙니다. 검사를 지나고 bonus(90)은 95, bonus()는 5를 찍게 하는 고치기를 모두 고르세요.

function bonus(
  score?: number,
) {
  return score + 5;
}
console.log(bonus(90));
console.log(bonus());
검사 결과 오류 1개
main.ts(4,10): error TS18048: 'score' is possibly 'undefined'.
  • ① 4번 줄을 return (score ?? 0) + 5;⁠로 바꾼다.
  • ② 4번 줄을 return score! + 5;로 바꾼다.
  • ③ 4번 줄을 if (score) { return score + 5; }⁠와 return 5; 두 줄로 바꾼다.
  • ④ 2번 줄의 score?:에서 ?를 지운다.
힌트

고친 코드마다 검사 결과와 bonus()의 실행 결과를 따로 따져 봅니다.

정답

①과 ③입니다. 둘 다 4번 줄에 오는 undefined를 실제로 거릅니다. ①은 없으면 0을 더해 5, ③은 갈래를 건너뛰어 5를 돌려줍니다(점수가 0이어도 0 + 5와 같은 5).

function bonus(
  score?: number,
) {
  return (score ?? 0) + 5;
}
console.log(bonus(90));
console.log(bonus());
실행 결과
95
5
  • ② 검사는 지나지만 bonus()가 NaN을 찍습니다. !는 검사기에게 하는 말일 뿐이라 undefined + 5가 그대로 계산됩니다(단언이 값을 바꾼다는 생각).
  • ④ 4번 줄의 오류는 사라지지만 7번 줄의 bonus()가 TS2554: Expected 1 arguments, but got 0.⁠으로 막힙니다. 없을 수 있는 값을 다루지 않고, 비우고 부르는 길을 없앤 것입니다.

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 1절 코드를 main.ts로 저장하고 npx tsc로 검사합니다. 2번 줄을 if (memo !== undefined)⁠로 바꾸고 마지막에 console.log(line(''));⁠을 더하면 무엇이 찍힐지 먼저 적고, node main.ts로 돌려 맞춰 봅니다. 편집기에서 3번 줄의 memo에 마우스를 올려 두 확인의 지금 타입이 같은지도 봅니다. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

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

타입스크립트 목차로