본문으로 건너뛰기

단원 연습 문제

타입스크립트 목차바깥에서 온 값 — any·unknown·as문제 12개

6단원 '바깥에서 온 값 — any·unknown·as'의 다섯 레슨에서 아홉 문제를 내고, 앞 단원(4 · 5단원)에서 세 문제를 섞었습니다. 앞 단원 문제가 어느 레슨인지는 정답을 펼치면 보입니다. 검사 결과는 지나가는지와 막히는 줄을, 지금 타입은 검사기가 찍는 글자를, 실행 결과는 찍히는 글자를 종이에 끝까지 쓴 뒤 정답을 펼치세요. 실행이 오류로 멈추면 오류 이름이 든 줄까지만 써도 됩니다. 막히면 정답 앞의 힌트를 먼저 펼칩니다. 점수는 매기지 않습니다.

한 번에 다 풀 필요는 없습니다. 문제 1~10은 폰으로도 풀 수 있고(약 30~40분), 끝의 만들어 보기 둘은 컴퓨터에서 tsc와 Node로 직접 검사하고 돌려 봅니다. 이 단원을 건너뛸지 보려면 진단 문항인 문제 1 · 3 · 5 · 6 · 8 다섯을 먼저 풀어 보세요(약 15분). 진단 문항은 힌트를 펴지 않고 풉니다. 힌트를 펴고 맞힌 문제나 짐작으로 맞힌 문제는 틀린 것으로 칩니다. 모두 맞으면 이 단원 레슨은 건너뛰고, 틀린 문제가 있으면 그 정답 끝의 레슨만 봅니다. 끝의 만들어 보기 둘은 골라서 풉니다.

타입 선언이 긴 문제와 만들어 보기는 코드를 블록 여럿으로 나눴습니다. 둘째 블록부터는 앞 블록에 이어 쓴 것이고 줄 번호도 이어서 셉니다. 끝의 두 문제는 바깥에서 온 JSON 글을 확인하고 쓰는 코드를 직접 만들어 봅니다. 컴퓨터가 있으면 블록을 차례로 main.ts에 이어 붙여 npx tsc로 검사 오류가 0인지 본 뒤 node main.ts로 돌려 맞춰 봅니다. 플레이그라운드에서는 맨 위에 export {}; 한 줄을 더합니다. 폰으로만 본다면 문제에 적힌 값으로 내 코드를 한 줄씩 검사하고 따라가 나와야 할 화면과 맞춰 봅니다. 검사 결과는 tsc 7.0.2로, 실행 결과는 Node 26으로 돌린 것입니다.

문제 1

걸음 수 앱이 저장해 둔 JSON 글에서 걸음 수를 꺼내, 오늘 더 걸은 2000보를 더해 찍습니다. 검사 결과와 실행 결과를 각각 쓰세요. 실행 결과는 마지막 줄까지 글자 그대로 씁니다.

const text =
  '{"steps":"6000"}';
const rec = JSON.parse(text);
const steps: number = rec.steps;
console.log(steps + 2000);
힌트

3번 줄 rec의 타입과, 2번 줄의 6000이 따옴표 안에 있는지 봅니다.

힌트 둘

JSON.parse는 글 내용과 상관없이 any를 돌려주고, : number는 실행 전에 지워집니다.

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

검사는 오류 없이 지나가고, 실행하면 60002000이 찍힙니다.

3번 줄의 rec는 JSON.parse가 돌려준 any라, rec.steps도 4번 줄의 number 자리에 검사 없이 들어갑니다(검사 규칙 6). 실행 때는 : number가 지워져 steps에 글자 '6000'이 그대로 붙고, 5번 줄의 +가 2000을 이어 붙입니다.

흔한 오답 하나는 8000입니다. number를 적으면 '6000'이 수로 바뀐다고 본 것입니다(타입이 실행 때 남는다는 생각). 4번 줄에서 막힌다고 썼다면 any도 검사한다고 본 것입니다(any가 무난하다는 생각).

JS 다시 보기: +는 한쪽이 글자면 둘을 이어 붙입니다(JS '수·글자·불리언과 typeof').

다시 볼 레슨: any는 검사를 끈다 · 타입은 실행 전에 지워진다

문제 2

종이책과 오디오북을 한 타입으로 받아, 저장해 둔 JSON 글을 읽어 보여 줍니다.

interface Paper {
  kind: 'paper';
  page: number;
}
interface Audio {
  kind: 'audio';
  min: number;
}
type Book = Paper | Audio;

위 타입 선언에 이어 씁니다. 14번 줄에서 b의 지금 타입은 무엇일까요? 검사 결과와 실행 결과도 각각 쓰세요. 실행 결과는 마지막 줄까지 글자 그대로 씁니다.

function show(b: Book) {
  if (b.kind === 'paper') {
    console.log(b.page + '쪽');
  } else {
    console.log(b.min + '분');
  }
}
const saved = [
  '{"kind":"paper","page":30}',
  '{"kind":"ebook","page":50}',
];
for (const s of saved) {
  show(JSON.parse(s));
}
힌트

22번 줄에서 show에 넘기는 값의 타입을 봅니다. 둘째 글은 11번 줄의 어느 갈래로 가는지 따라갑니다.

힌트 둘

any는 Book 자리에도 검사 없이 들어갑니다. 실행 때는 타입이 지워져, kind가 'paper'가 아닌 값은 모두 else 갈래로 갑니다.

정답

14번 줄에서 b의 지금 타입은 Audio이고, 검사는 오류 없이 지나갑니다.

검사 결과 오류 없음
실행 결과
30쪽
undefined분
줄이름지금 타입
10bBook
11bBook → 참 Paper 거짓 Audio
12bPaper
14bAudio

검사기는 10번 줄의 Book을 믿어 거짓 갈래인 14번 줄을 Audio로 봅니다(검사 규칙 5). 22번 줄의 JSON.parse(s)는 any라 'ebook' 글도 막힘없이 들어갑니다(검사 규칙 6). 실행 때 둘째 값은 'paper'가 아니라 else 갈래로 가고, min이 없어 undefined에 '분'이 붙습니다.

흔한 오답 하나는 22번 줄에서 막힌다고 쓴 것입니다(any가 무난하다는 생각). 둘째 줄을 비우거나 오류로 멈춘다고 썼다면 실행 때도 b가 Paper나 Audio뿐이라고 본 것입니다(타입이 실행 때 남는다는 생각).

JS 다시 보기: 없는 속성을 읽으면 undefined입니다(JS '객체: 이름 붙은 칸 묶음').

다시 볼 레슨: 판별 유니언 · any는 검사를 끈다 · 타입은 실행 전에 지워진다

문제 3

카페 주문 앱이 받은 JSON 글이 중간에 끊겼습니다. 잔 수를 꺼내다 오류가 나면 오류 메시지를 찍습니다. 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.

const raw = '{"cups":';
try {
  const order = JSON.parse(raw);
  console.log(order.cups);
} catch (e) {
  console.log(e.message);
  if (e instanceof Error) {
    console.log(e.name);
  }
}
힌트

4 · 6 · 8번 줄마다 점 앞의 이름(order · e)이 그 줄에서 어떤 타입인지 봅니다.

힌트 둘

JSON.parse는 any를 돌려주고, catch가 받는 e는 unknown입니다. unknown은 확인해 좁힌 갈래 안에서만 속성을 꺼낼 수 있습니다.

정답
검사 결과 오류 1개
main.ts(6,15): error TS18046: 'e' is of type 'unknown'.

6번 줄에서만 막힙니다.

  • 4번 줄: order는 JSON.parse가 돌려준 any라 검사하지 않습니다(검사 규칙 6).
  • 6번 줄: throw는 글자나 수도 던질 수 있어 catch의 e는 unknown이고(TS18046 is of type 'unknown'), 확인 전에는 message를 꺼낼 수 없습니다.
  • 8번 줄: 7번 줄의 e instanceof Error로 e가 Error로 좁혀집니다.

흔한 오답 하나는 4번 줄도 막힌다고 쓴 것입니다(any가 무난하다는 생각). '지나감'이라고 쓰면 catch의 e를 늘 Error로 본 것이고, 8번 줄까지 쓰면 instanceof가 e를 좁히지 못한다고 본 것입니다.

JS 다시 보기: 끊긴 글을 JSON.parse가 읽으면 SyntaxError를 던지고 catch가 받습니다(JS 'JSON: 객체와 글 사이' · JS 'try와 catch').

다시 볼 레슨: unknown — 확인하고 쓰기 · any는 검사를 끈다

문제 4

카페 정산 앱이 바깥에서 온 값 둘(낸 돈 won · 잔 수 qty)로 한 잔 값을 셈합니다. 둘 가운데 수가 아닌 값이 있으면 걸러 내려 합니다. 11번 줄에서 won과 qty의 지금 타입은 각각 무엇일까요? 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.

function unit(
  won: unknown,
  qty: unknown,
) {
  if (typeof won !== 'number') {
    return 0;
  }
  if (typeof qty !== 'number') {
    console.log('개수 없음');
  }
  return won / qty;
}
console.log(unit(9000, 3));
힌트

타입 추적 표에 2 · 3 · 5 · 8 · 11번 줄의 won과 qty를 적어 봅니다.

힌트 둘

5번 줄과 8번 줄의 갈래가 각각 return으로 끝나는지 봅니다. return으로 끝난 길은 다음 줄로 오지 않습니다.

정답

11번 줄에서 won의 지금 타입은 number, qty는 unknown이고, 검사는 11번 줄에서 막힙니다.

검사 결과 오류 1개
main.ts(11,16): error TS18046: 'qty' is of type 'unknown'.
줄이름지금 타입
2wonunknown
3qtyunknown
5wonunknown → 참 unknown 거짓 number
8won만나는 줄 — 참은 오지 않음(6번 줄 return) + 거짓 number → number
8qtyunknown → 참 unknown 거짓 number
11wonnumber
11qty만나는 줄 — 참 unknown + 거짓 number → unknown

5번 줄의 참 갈래는 return으로 끝나, won은 8번 줄부터 number입니다. 8번 줄의 참 갈래는 return이 없어 qty가 수가 아닌 길도 11번 줄로 오고, unknown과 number가 만나면 unknown이라 나눌 수 없습니다(검사 규칙 5).

흔한 오답 하나는 qty를 number로 쓰고 '지나감'이라고 쓴 것입니다(좁힘이 끝까지 간다는 생각). won도 unknown으로 썼다면 if 블록을 나오면 2번 줄의 타입으로 돌아간다고 본 것입니다(갈래를 나오면 선언 타입이라는 생각).

다시 볼 레슨: 좁히기 — typeof와 타입 추적 표 · unknown — 확인하고 쓰기

문제 5

카페 도장 앱이 저장해 둔 도장 수를 꺼내 3을 더해 찍습니다. 검사 결과와 실행 결과를 각각 쓰세요. 실행 결과는 마지막 줄까지 글자 그대로 씁니다.

const saved: unknown = '7';
const a = saved as number;
const b = '7' as number;
console.log(a + 3);
console.log(b + 3);
힌트

2번 줄과 3번 줄에서 as 앞에 놓인 값의 타입이 각각 무엇인지 봅니다.

힌트 둘

as는 검사기에게 그 타입이라고 말할 뿐 값을 바꾸지 않습니다. 검사기는 서로 넣을 수 없는 두 타입(string과 number처럼) 사이의 as만 막습니다.

정답
검사 결과 오류 1개
main.ts(3,11): error TS2352: Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
실행 결과
73
73

검사는 3번 줄에서만 막힙니다. TS2352는 string과 number가 겹치는 값이 없어(sufficiently overlaps) 실수 같다는 알림입니다. 2번 줄의 saved는 unknown이라 무엇으로든 단언할 수 있습니다. Node는 검사 오류와 상관없이 돌고 as는 지워집니다. a와 b에는 글자 '7'이 그대로 있어 두 줄 모두 73입니다.

흔한 오답 하나는 10과 10입니다. as가 '7'을 수로 바꾼다고 본 것입니다(단언이 값을 바꾼다는 생각). 실행 결과를 비웠다면 검사 오류 때문에 돌지 않는다고 본 것입니다(검사 오류가 실행을 막는다는 생각). 2번 줄도 막힌다고 쓰면 saved의 타입을 1번 줄의 unknown이 아니라 든 값 '7'로 본 것입니다.

JS 다시 보기: +는 한쪽이 글자면 이어 붙이고, Number('7')은 글자를 수 7로 바꿉니다(JS '수·글자·불리언과 typeof' · JS '글자를 수로: Number와 NaN').

다시 볼 레슨: as는 검사기에게 하는 말 · 타입은 실행 전에 지워진다

문제 6

성적 앱이 바깥에서 온 값이 성적(Grade)인지 확인합니다. 받은 글이 'null'이면 false를 찍어야 하는데, 검사에서 막히고 돌리면 멈춥니다.

interface Grade {
  avg: number;
}
function isGrade(
  v: unknown,
): v is Grade {
  return (
    typeof v === 'object' &&
    'avg' in v &&
    typeof v.avg === 'number'
  );
}
const g = JSON.parse('null');
console.log(isGrade(g));
검사 결과 오류 1개
main.ts(9,14): error TS18047: 'v' is possibly 'null'.
실행 결과
main.ts:9    'avg' in v &&          ^ TypeError: Cannot use 'in' operator to search for 'avg' in null    at isGrade (main.ts:9:11)    at main.ts:14:13

아래 고치기 가운데 검사 오류를 없애는 것을 모두 고르세요. 그 가운데 false를 찍는 것도 모두 고르세요.

  • ① 9번 줄 앞에 // @ts-ignore 한 줄을 더한다
  • ② 8번 줄을 v instanceof Grade &&⁠로 고친다
  • ③ 8번 줄 뒤에 v !== null && 한 줄을 더한다
  • ④ 5번 줄을 v: any,로 고친다
힌트

13번 줄의 g에 어떤 값이 붙는지 보고, 그 값이 8번 줄을 지나는지 따라가 봅니다.

힌트 둘

typeof null은 'object'입니다. 고치기마다 검사기가 9번 줄의 v를 어떻게 보는지와, 실행 때 null이 어디서 걸러지는지 나눠 봅니다.

정답

검사 오류를 없애는 것은 ①③④이고, false를 찍는 것은 ③뿐입니다.

8번 줄의 typeof v === 'object'⁠는 v가 null이어도 참이라, 9번 줄의 v는 null일 수 있습니다(TS18047 possibly 'null'). 13번 줄의 g가 바로 null이라 9번 줄의 in에서 멈췄습니다. ③은 null을 걸러 v를 object로 좁히고, null이면 9번 줄 전에 false를 돌려줍니다.

  • ①은 오류만 감춰, null이 9번 줄에 와 같은 TypeError로 멈춥니다.
  • ②는 TS2693으로 막힙니다. interface인 Grade는 타입만 가리켜(only refers to a type) 실행 때 없는 이름이라, 돌리면 ReferenceError로 멈춥니다.
  • ④는 any로 검사를 끈 것이라 ①처럼 멈춥니다.

흔한 오답 하나는 ③을 빼고 고른 것입니다(object면 null이 아니라는 생각). ①이나 ④도 false를 찍는다고 골랐다면 검사를 지나면 null도 걸러진다고 본 것입니다(검사를 지나면 맞다는 생각). ②를 고르면 interface도 instanceof로 확인된다고 본 것입니다(instanceof로 타입을 확인하는 생각).

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

다시 볼 레슨: 직접 만드는 확인 함수 · any는 검사를 끈다 · 타입 오류 읽기

문제 7

성적 앱이 바깥에서 온 값이 학기(Term)인지 확인합니다. isTerm은 v가 수 1이나 2일 때만 true를 돌려줘야 합니다. 이렇게 나와야 합니다.

true
true
false
false
false

검사를 지나고 실행 결과가 위처럼 되도록 5번 줄의 빈칸을 채우세요. as · any · // @ts-ignore로 덮은 답은 받지 않습니다.

type Term = 1 | 2;
function isTerm(
  v: unknown,
): v is Term {
  return ____;
}
console.log(isTerm(1));
console.log(isTerm(2));
console.log(isTerm(3));
console.log(isTerm('2'));
console.log(isTerm(1.5));
힌트

7~11번 줄에 넘기는 값 다섯이 각각 수인지 글자인지 봅니다.

힌트 둘

Term은 타입이라 실행 전에 지워집니다. 실행 때 남는 값 1 · 2와 견주되, isTerm('2')는 false여야 합니다.

정답

받는 답입니다.

type Term = 1 | 2;
function isTerm(
  v: unknown,
): v is Term {
  return v === 1 || v === 2;
}
console.log(isTerm(1));
console.log(isTerm(2));
console.log(isTerm(3));
console.log(isTerm('2'));
console.log(isTerm(1.5));
실행 결과
true
true
false
false
false

Term은 타입이라 실행 전에 지워지니, 실행 때 남는 값 1 · 2와 ===로 견줍니다. 글자 '2'는 종류가 달라 false입니다. 검사기는 v is Term이라는 말을 믿을 뿐 식이 맞는지 보지 않으니(검사 규칙 6), 시험 값으로 화면을 맞춰 봅니다.

검사를 지나고 v가 수 1이나 2일 때만 true가 되는 식이면 맞습니다. 차례를 바꾼 식, 괄호로 묶은 식, typeof로 수인지 먼저 확인하고 &&로 이은 식도 같은 답입니다.

틀리는 꼴:

  • v == 1 || v == 2 — 검사는 지나지만 ==가 글자 '2'를 수로 바꿔 넷째 줄이 true입니다(검사를 지나면 맞다는 생각).
  • typeof v === 'number' — 검사는 지나지만 셋째 · 다섯째 줄이 true입니다. 범위(v >= 1과 v <= 2)로 견줘도 1.5가 true입니다.
  • v === Term · v instanceof Term — TS2693으로 막히고 돌리면 ReferenceError입니다. Term은 타입만 가리킵니다(타입과 값을 섞는 생각 · instanceof로 타입을 확인하는 생각).
  • [1, 2].includes(v) · typeof v === 'Term' — TS2345 · TS2367로 막힙니다. includes는 unknown인 v를 받지 않고, typeof는 'number' 같은 정해진 글자만 돌려줘 겹치지 않습니다(have no overlap).

JS 다시 보기: ==는 견주기 전에 값의 종류를 바꿀 수 있고, ===는 바꾸지 않습니다(JS '같음과 비교').

다시 볼 레슨: 직접 만드는 확인 함수 · 리터럴 유니언으로 값 고르기

문제 8

카페 앱이 JSON 글을 읽는 함수 parseRaw를 만들었습니다. 깨진 글이면 null을 돌려줍니다. 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.

function parseRaw(s: string) {
  try {
    return JSON.parse(s);
  } catch {
    return null;
  }
}
const v =
  parseRaw('{"cups":"4"}');
console.log(v.cups + 1);
힌트

1번 줄에 parseRaw의 반환 타입이 적혀 있는지 봅니다. 3번 줄과 5번 줄이 돌려주는 값의 타입을 봅니다.

힌트 둘

반환 타입을 적지 않으면 검사기가 return 하는 값들로 정합니다. any에 무엇을 합쳐도 any입니다.

정답
검사 결과 오류 없음

검사 오류 없이 지나갑니다. 1번 줄에 반환 타입이 없어 검사기가 return 하는 값으로 정하는데, 3번 줄의 any와 5번 줄의 null을 합쳐도 any입니다. 그래서 8번 줄의 v도 any라 10번 줄을 검사하지 않습니다(검사 규칙 6). 바깥 값의 any가 함수 밖으로 샌 것입니다. 돌리면 글자 '4'에 1이 이어 붙어 41입니다.

흔한 오답은 10번 줄에서 막힌다고 쓴 것입니다. 함수로 감싸면 검사기가 값을 살핀다고 본 것입니다(any가 무난하다는 생각). 레슨처럼 반환 타입 : unknown을 적어야 v가 unknown이 되어 v.cups 줄이 TS18046으로 막힙니다.

다시 볼 레슨: 바깥 값 확인하기 · any는 검사를 끈다

문제 9

하고 싶은 일마다 쓸 타입이나 낱말을 쓰세요.

  • (가) 무엇이 들었는지 모르는 값을 받되, 확인하기 전에는 쓰지 못하게 하는 타입
  • (나) 검사기에게 '이 값은 이 타입이다'라고 말하는 낱말(값은 바꾸지 않음)
  • (다) 확인 함수의 반환 타입 자리에 v ____ Order처럼 써서, true면 v가 Order라고 알리는 낱말
  • (라) 객체에 그 이름의 속성이 있는지 'qty' ____ v처럼 확인하는 연산자
힌트

(가)는 any와 견주어 배운 타입입니다. (다)와 (라)는 확인 함수 isOrder에 모두 나옵니다.

정답

받는 답: (가) unknown · (나) as · (다) is · (라) in

같은 답으로 치는 꼴: (다)를 v is Order처럼 통째로 쓴 것.

틀리는 꼴:

  • (가) any — 무엇이든 받지만 확인 없이도 쓰게 해 검사를 끕니다.
  • (나) is — 확인 함수의 반환 타입에 쓰는 낱말입니다.
  • (다) as — 값 뒤에 붙여 단언하는 낱말입니다.
  • (라) instanceof — 클래스로 만든 객체인지 봅니다. 속성이 있는지는 in으로 봅니다.

다시 볼 레슨: unknown — 확인하고 쓰기 · as는 검사기에게 하는 말 · 직접 만드는 확인 함수

문제 10

운동 앱이 하루 기록(JSON 글)의 km에 1을 더해 내일 목표로 찍습니다. km가 수가 아닌 기록은 건너뛰어, 아래 두 기록이면 둘째 날의 4 한 줄만 찍혀야 합니다. 그런데 검사에서 막힙니다.

코드와 검사 결과입니다. 몇 번 줄의 무엇을 고쳐야 할까요? 고친 줄을 쓰세요.

function goal(v: unknown) {
  if (
    typeof v === 'object' &&
    v !== null &&
    'km' in v &&
    v.km !== ''
  ) {
    console.log(v.km + 1);
  }
}
const day1 = '{"km":"5"}';
const day2 = '{"km":3}';
goal(JSON.parse(day1));
goal(JSON.parse(day2));
검사 결과 오류 1개
main.ts(8,17): error TS18046: 'v.km' is of type 'unknown'.
힌트

8번 줄에서 v.km의 타입이 몇 번 줄의 확인으로 정해지는지 거슬러 갑니다.

힌트 둘

3~6번 줄의 확인 넷이 각각 무엇을 거르는지 봅니다. 8번 줄에서 1을 더하려면 v.km가 어떤 타입이어야 하는지 봅니다.

정답

6번 줄을 typeof v.km === 'number'⁠로 고칩니다.

function goal(v: unknown) {
  if (
    typeof v === 'object' &&
    v !== null &&
    'km' in v &&
    typeof v.km === 'number'
  ) {
    console.log(v.km + 1);
  }
}
const day1 = '{"km":"5"}';
const day2 = '{"km":3}';
goal(JSON.parse(day1));
goal(JSON.parse(day2));
실행 결과
4

오류는 8번 줄에서 났지만 까닭은 6번 줄입니다. 5번 줄의 'km' in v는 km가 있다는 것만 알려 줘 v.km는 unknown이고, 6번 줄의 v.km !== ''는 타입을 좁히지 않습니다. typeof 확인으로 바꾸면 v.km가 number로 좁혀지고, 글자 '5'인 첫째 날은 걸러집니다.

  • 다르게 고쳐도 되는 곳: 8번 줄을 if (typeof v.km === 'number') 갈래 안에 넣어도 됩니다.

흔한 오답은 오류가 난 8번 줄만 덮는 것입니다. 8번 줄 위에 // @ts-ignore를 더한 코드와 실행 결과입니다.

function goal(v: unknown) {
  if (
    typeof v === 'object' &&
    v !== null &&
    'km' in v &&
    v.km !== ''
  ) {
    // @ts-ignore
    console.log(v.km + 1);
  }
}
const day1 = '{"km":"5"}';
const day2 = '{"km":3}';
goal(JSON.parse(day1));
goal(JSON.parse(day2));
실행 결과
51
4

검사는 지나지만 글자 '5'가 걸러지지 않아 51까지 찍힙니다. v.km as number나 1번 줄의 any도 같은 51과 4입니다. Number(v.km) + 1은 6과 4를 찍어, 수가 아닌 기록은 건너뛴다는 뜻과 다릅니다.

덮는 길은 검사 결과에서 오류만 지우고 값과 실행 결과는 그대로 둡니다(오류 줄이 고칠 줄이라는 생각 · 검사를 지나면 맞다는 생각).

JS 다시 보기: +는 한쪽이 글자면 둘을 이어 붙입니다(JS '수·글자·불리언과 typeof').

다시 볼 레슨: 직접 만드는 확인 함수 · as는 검사기에게 하는 말 · 타입 오류 읽기

문제 11 (만들어 보기)

카페 충전 기록(JSON 글) 가운데 맞는 것만 더하는 합계를 만들어 봅니다. 충전 기록은 {"won":10000}처럼 won에 수가 든 객체여야 합니다. 다 채워 돌리면 이렇게 나와야 합니다.

충전 13000원

뼈대입니다. 주석 넷 아래를 채우세요. 검사 오류는 0이어야 하고, 깨진 글과 won이 수가 아닌 글은 더하지 않습니다.

interface Charge {
  won: number;
}
function parseRaw(
  s: string,
): unknown {
  // 1. unknown으로 받기
}
function isCharge(
  v: unknown,
): v is Charge {
  // 2. 객체인지(null 아님)
  // 3. 속성마다 있는지와 타입
}

앞 코드에 이어 씁니다.

const lines = [
  '{"won":10000}',
  '{"won":"5000"}',
  '{"won":3000}',
  '{"won":',
];
let sum = 0;
for (const s of lines) {
  // 4. 확인한 뒤 쓰기
}
console.log(`충전 ${sum}원`);

다 쓰면 둘째 글을 '{"won":5000}'로 바꿔 따라가 봅니다. 충전 18000원이어야 합니다.

힌트

'1. unknown으로 받기'는 try와 catch로 JSON.parse를 감쌉니다. '2.'와 '3.'은 return 하나에 &&로 잇습니다. '4.'에서 won은 isCharge가 참인 갈래 안에서만 씁니다.

정답

모범 답입니다. 첫째 블록의 두 함수 가운데 parseRaw부터 채웁니다.

interface Charge {
  won: number;
}
function parseRaw(
  s: string,
): unknown {
  // 1. unknown으로 받기
  try {
    return JSON.parse(s);
  } catch {
    return null;
  }
}

채운 isCharge를 앞 코드에 이어 씁니다.

function isCharge(
  v: unknown,
): v is Charge {
  return (
    // 2. 객체인지(null 아님)
    typeof v === 'object' &&
    v !== null &&
    // 3. 속성마다 있는지와 타입
    'won' in v &&
    typeof v.won === 'number'
  );
}
검사 결과 오류 없음

채운 둘째 블록을 앞 코드에 이어 씁니다.

const lines = [
  '{"won":10000}',
  '{"won":"5000"}',
  '{"won":3000}',
  '{"won":',
];
let sum = 0;
for (const s of lines) {
  // 4. 확인한 뒤 쓰기
  const v = parseRaw(s);
  if (isCharge(v)) {
    sum = sum + v.won;
  }
}
console.log(`충전 ${sum}원`);
실행 결과
충전 13000원

둘째 글을 '{"won":5000}'로 바꾸면 won이 수라 isCharge가 참이 되어 5000도 더해지고, 충전 18000원입니다.

  • 다르게 써도 되는 곳: isCharge를 return 하나 대신 if로 하나씩 걸러 false를 돌려주는 꼴로 써도 됩니다. 합계는 sum += v.won으로 써도 같습니다.
  • 검사 오류가 0이고, 찍은 화면이 위와 같고, 둘째 글을 바꾼 18000원도 맞으면 맞습니다.
  • 흔한 오답 하나는 won의 typeof 확인을 뺀 것입니다. 검사는 지나지만 글자 '5000'이 이어 붙어 충전 1000050003000원이 찍힙니다. 검사기는 v is Charge라는 말을 믿을 뿐, 속성을 빠짐없이 확인했는지는 보지 않습니다.
  • isCharge 없이 parseRaw(s) as Charge⁠로 단언하면 검사는 지나지만, 깨진 넷째 글에서 parseRaw가 돌려준 null의 won을 읽다 TypeError로 멈춥니다.
  • parseRaw에서 try와 catch를 빼면 넷째 글에서 JSON.parse가 SyntaxError를 던져 멈춥니다.

다시 볼 레슨: 바깥 값 확인하기 · 직접 만드는 확인 함수 · unknown — 확인하고 쓰기

문제 12 (만들어 보기)

이번에는 빈 데서 씁니다. 카페 픽업 화면이 JSON 글 넷을 차례로 읽어, {"who":"민지"}처럼 who에 글자가 든 글이면 그 이름을, 아니면 '확인 못 함'을 한 줄씩 찍습니다. 호출 타입 Call, 함수 parseRaw와 isCall, 글 넷을 도는 반복을 쓰세요. 글 넷은 차례로 '{"who":"민지"}' · '{"who":3}' · '{"who":"준호"}' · '{"who"'입니다. 이렇게 나와야 합니다. 검사 오류는 0이어야 합니다.

민지
확인 못 함
준호
확인 못 함

다 쓰면 둘째 글을 '{"who":"3"}'로 바꿔 따라가 봅니다. 둘째 줄이 3이어야 합니다.

힌트

하위 목표 넷(1. unknown으로 받기 · 2. 객체인지(null 아님) · 3. 속성마다 있는지와 타입 · 4. 확인한 뒤 쓰기)을 따릅니다. 문제 11과 같은 틀이고, 여기서는 who가 글자인지 봅니다.

정답

모범 답입니다.

interface Call {
  who: string;
}
function parseRaw(
  s: string,
): unknown {
  try {
    return JSON.parse(s);
  } catch {
    return null;
  }
}

앞 코드에 이어 씁니다.

function isCall(
  v: unknown,
): v is Call {
  return (
    typeof v === 'object' &&
    v !== null &&
    'who' in v &&
    typeof v.who === 'string'
  );
}
검사 결과 오류 없음

앞 코드에 이어 씁니다.

const lines = [
  '{"who":"민지"}',
  '{"who":3}',
  '{"who":"준호"}',
  '{"who"',
];
for (const s of lines) {
  const v = parseRaw(s);
  if (isCall(v)) {
    console.log(v.who);
  } else {
    console.log('확인 못 함');
  }
}
실행 결과
민지
확인 못 함
준호
확인 못 함

4~12번 줄이 '1. unknown으로 받기', 17~18번 줄이 '2. 객체인지(null 아님)', 19~20번 줄이 '3. 속성마다 있는지와 타입', 30~35번 줄이 '4. 확인한 뒤 쓰기'입니다. 둘째 글을 '{"who":"3"}'로 바꾸면 who가 글자 '3'이라 isCall이 참이 되어 둘째 줄이 3입니다.

  • 다르게 써도 되는 곳: if (!isCall(v))로 확인 못 한 글을 먼저 찍고 else에서 이름을 찍어도 같은 화면이 나옵니다.
  • 검사 오류가 0이고, 찍은 화면이 위와 같고, 둘째 글을 바꾼 3도 맞으면 맞습니다.
  • 흔한 오답 하나는 who의 typeof 확인을 뺀 것입니다. 검사는 지나지만 수 3도 이름으로 받아 둘째 줄에 3이 찍힙니다.
  • v !== null을 빼면 검사가 'who' in v 줄에서 TS18047로 막힙니다. 그대로 돌리면 셋째 줄까지 찍은 뒤, 깨진 넷째 글에서 parseRaw가 돌려준 null에 in을 쓰다 TypeError로 멈춥니다.

다시 볼 레슨: 바깥 값 확인하기 · 직접 만드는 확인 함수

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

타입스크립트 목차로