본문으로 건너뛰기

단원 1~5 섞어 풀기

타입스크립트 목차갈래가 있는 데이터문제 12개

하루 지나 풀면 더 오래 남습니다. 1~5단원에서 열두 문제를 단원 차례 없이 섞었습니다. 문제마다 어느 레슨인지는 정답을 펼치면 보입니다. 검사 결과는 지나가는지와 막히는 줄을, 지금 타입은 검사기가 찍는 글자를, 실행 결과는 찍히는 글자를 종이에 끝까지 쓴 뒤 정답을 펼치세요. 실행이 오류로 멈추면 오류 이름이 든 줄까지만 써도 됩니다. 막히면 정답 앞의 힌트를 먼저 펼칩니다. 점수는 매기지 않습니다.

한 번에 다 풀 필요는 없습니다. 문제 1~11은 폰으로도 풀 수 있고(약 30~40분), 끝의 만들어 보기 하나는 컴퓨터에서 tsc와 Node로 직접 검사하고 돌려 봅니다.

긴 문제 둘(6 · 11)은 타입 추적 표를 끝까지 그려 풉니다. 지금 타입은 유니언의 차례와 리터럴의 따옴표 모양이 정답과 달라도 같은 답입니다. 마지막 문제는 작은 주문표를 혼자 써 봅니다. 타입 선언이 긴 코드는 블록을 나눴고, 둘째 블록부터는 앞 블록에 이어 쓴 것이라 줄 번호도 이어서 셉니다. 컴퓨터가 있으면 블록을 차례로 main.ts에 이어 붙여 npx tsc로 검사 오류가 0인지 본 뒤 node main.ts로 돌려 맞춰 봅니다. 폰으로만 본다면 문제에 적힌 값으로 내 코드를 한 줄씩 검사하고 따라가 맞춰 봅니다. 검사 결과는 tsc 7.0.2로, 실행 결과는 Node 26으로 돌린 것입니다.

문제 1

에스프레소 샷은 single과 double 둘뿐입니다. 5 · 6번 줄에서 a와 b의 지금 타입은 무엇일까요? 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.

type Shot = 'single' | 'double';
function brew(s: Shot) {
  return s + ' 샷';
}
const a = 'double';
let b = 'single';
console.log(brew(a));
console.log(brew(b));
힌트

5 · 6번 줄에서 a · b의 지금 타입을 먼저 쓰고, 7 · 8번 줄에서 받는 자리 Shot과 견줘 봅니다.

힌트 둘

5번 줄과 6번 줄은 이름을 만드는 낱말이 다릅니다.

정답

a는 "double", b는 string이고, 검사는 8번 줄에서 막힙니다.

검사 결과 오류 1개
main.ts(8,18): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Shot'.

const로 만든 글자 하나는 그 값 하나짜리 타입이라 a는 Shot에 듭니다. let으로 만든 b는 뒤에 다른 글자에 다시 붙을 수 있어 string으로 넓어지고, string은 Shot보다 큰 모음이라 8번 줄에서 넣을 수 없습니다(검사 규칙 3). 고칠 곳은 6번 줄입니다 — const로 바꾸거나 let b: Shot =처럼 타입을 적습니다.

같은 답으로 치는 꼴: a를 'double'처럼 작은따옴표로 쓴 것.

흔한 오답은 b를 "single"로 쓰고 '지나감'이라고 쓴 것입니다. let도 첫 값 그대로의 타입이라고 본 것입니다(넓어지지 않는다는 생각).

다시 볼 레슨: 적지 않아도 붙는 타입 — 추론 · 리터럴 유니언으로 값 고르기

문제 2

수영한 바퀴 수에 한 바퀴를 더합니다. 검사 결과와 실행 결과를 각각 쓰세요.

let laps: number = 0;
laps = '3';
laps = laps + 1;
console.log(laps);
힌트

검사기와 Node가 2번 줄에서 각각 무엇을 하는지 따로 봅니다.

정답
검사 결과 오류 1개
main.ts(2,1): error TS2322: Type 'string' is not assignable to type 'number'.
실행 결과
31

검사는 2번 줄에서 막힙니다. laps의 선언 타입 number 자리에 글자 '3'을 넣으려 했습니다. 3번 줄은 선언 타입 number로 보고 지나갑니다. Node는 타입 글자를 지우고 돌립니다. laps가 글자 '3'에 붙고, 3번 줄의 +가 1을 이어 붙여 '31'을 찍습니다.

흔한 오답 하나는 '아무것도 찍지 않는다'입니다. 검사 오류가 있으면 Node가 돌지 않는다고 본 것입니다(검사 오류가 실행을 막는다는 생각). 다른 하나는 4입니다. 타입을 number로 적었으니 '3'이 수 3으로 바뀐다고 본 것입니다(타입이 실행 때 남는다는 생각). 타입은 검사기만 보고 지워지며, 값을 막지도 바꾸지도 않습니다(검사 규칙 1).

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

다시 볼 레슨: 타입은 실행 전에 지워진다

문제 3

오늘 수영한 거리는 바퀴 수 × 수영장 길이로 500m여야 합니다. 검사 결과와 실행 결과를 각각 쓰세요.

interface Swim {
  laps: number;
  pool: number;
}
const s: Swim = {
  laps: 20,
  pool: 25,
};
const meters = s.laps + s.pool;
console.log(meters + 'm');
힌트

10번 줄이 찍는 meters가 몇 번 줄에서 무엇으로 만들어지는지 봅니다.

힌트 둘

9번 줄의 계산을 손으로 해 봅니다.

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

검사는 오류 없이 지나지만 45m가 찍힙니다. 9번 줄에서 곱할 자리에 더하기를 썼습니다. laps와 pool이 둘 다 number라 더해도 타입은 맞아, 검사기는 알려 주지 않습니다(검사 규칙 1). s.laps * s.pool로 고치면 500m입니다.

흔한 오답은 실행 결과를 500m로 쓴 것입니다. 검사를 지나면 뜻대로 돈다고 본 것입니다(검사를 지나면 맞다는 생각). 바로 앞 문제와 짝입니다 — 검사에서 막혀도 실행은 돌고, 검사를 지나도 결과는 틀릴 수 있습니다. 두 결과는 따로 봅니다.

다시 볼 레슨: 타입 검사기가 하는 일

문제 4

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

  • (가) 이름을 만들 때 그 이름의 타입을 적으려고 이름 바로 뒤에 쓰는 기호
  • (나) 검사기가 다시 대입하기를 막게 속성 이름 앞에 붙이는 낱말
  • (다) 값이 하나도 들지 않는 타입
  • (라) 첫 칸은 글자, 둘째 칸은 수로 정해진 두 칸짜리 배열의 타입
힌트

(다)는 갈래를 빠짐없이 처리했는지 보는 자리에 씁니다.

정답

받는 답: (가) : · (나) readonly · (다) never · (라) [string, number]

틀리는 꼴:

  • (가) 구조 분해 중괄호 안의 콜론은 같은 기호지만 타입이 아니라 이름을 바꿔 붙이는 표시입니다. 구조 분해로 받는 매개변수의 타입은 중괄호 뒤에 적습니다(}: Menu).
  • (나) const — 이름을 다른 값에 다시 붙이는 것만 막습니다.
  • (다) void — 돌려주는 값이 없는 함수의 타입입니다.
  • (라) 글자와 수가 섞인 배열 리터럴에서 추론되는 타입은 칸 수와 차례를 정하지 않아 튜플이 아닙니다.

다시 볼 레슨: 타입 적기와 넣을 수 있는지 검사 · 선택 속성과 readonly · 빠짐없이 — never로 갈래 검사 · 배열과 튜플

문제 5

메뉴마다 받은 표를 모았습니다. 검사 결과와 실행 결과를 각각 쓰세요.

interface Votes {
  [menu: string]: number;
}
const votes: Votes = { 라테: 3 };
const mocha = votes['모카'];
console.log(mocha.toFixed(0));
힌트

5번 줄의 mocha를 검사기가 무엇으로 보는지와, 실행 때 '모카' 칸이 정말 있는지를 따로 봅니다.

정답
검사 결과 오류 없음
실행 결과
main.ts:6console.log(mocha.toFixed(0));                  ^ TypeError: Cannot read properties of undefined (reading 'toFixed')    at main.ts:6:19

검사는 오류 없이 지나갑니다. 인덱스 시그니처는 '어떤 이름의 칸이든 값은 number'라는 뜻이라, mocha의 지금 타입은 number이고 toFixed가 있다고 봅니다. 실행은 6번 줄에서 멈춥니다. votes에는 라테 칸뿐이라 mocha는 undefined이고, undefined에서 toFixed를 꺼내려다 TypeError가 납니다.

흔한 오답은 실행 결과를 0으로 쓴 것입니다. 검사가 number라 했으니 값이 있다고 본 것입니다(꺼낸 값은 늘 있다는 생각). 없을 수 있는 칸은 ?? 0처럼 실행할 때 거릅니다(검사 규칙 2).

다시 볼 레슨: 키가 정해지지 않은 객체 — 인덱스 시그니처 · 배열과 튜플

문제 6

쿠폰 코드를 글로 바꾸는 함수입니다. 줄마다 v와 text의 지금 타입을 타입 추적 표로 적고, 검사를 지나갈지 쓰세요. 지나가지 못하면 막히는 줄을 모두 씁니다.

function tag(
  v: string | number | null,
) {
  if (v === null) {
    return '없음';
  }
  let text = '';
  if (typeof v === 'number') {
    text = v.toFixed(1);
  } else {
    text = v.toUpperCase();
  }
  return text + v.length;
}
힌트

확인하는 줄(4 · 8)마다 참 · 거짓 길을 나누고, 길이 만나는 줄(8 · 13)에는 오는 길마다 타입을 적어 합칩니다.

힌트 둘

return으로 끝난 길은 오지 않습니다.

정답

검사는 13번 줄에서 막힙니다.

검사 결과 오류 1개
main.ts(13,19): error TS2339: Property 'length' does not exist on type 'string | number'.  Property 'length' does not exist on type 'number'.
줄이름지금 타입
2vstring | number | null
4vstring | number | null → 참 null 거짓 string | number
7textstring(첫 값 ''에서 넓힘)
8v만나는 줄 — 참은 오지 않음(5번 줄 return) + 거짓 string | number → string | number
8vstring | number → 참 number 거짓 string
9vnumber
9text대입 — 선언 타입 string으로 검사, 지나감
11vstring
11text대입 — 선언 타입 string으로 검사, 지나감
13v만나는 줄 — 참 number + 거짓 string → string | number
13textstring

8번 줄의 갈래는 if · else 모두 return 없이 끝나, 13번 줄로 두 길이 옵니다. 그래서 13번 줄의 v는 다시 string | number이고, number에는 length가 없어 막힙니다(검사 규칙 5). text는 두 갈래 모두 글자에 다시 붙어 string입니다.

흔한 오답은 13번 줄의 v를 string으로 보고 '지나감'이라고 쓴 것입니다. else 갈래가 마지막이니 그 좁힘이 이어진다고 본 것입니다(좁힘이 끝까지 간다는 생각). 13번 줄의 v를 string | number | null⁠로 쓰면, 갈래를 지난 v가 2번 줄에 적은 타입 그대로라고 본 것입니다(갈래를 나오면 선언 타입이라는 생각). null 길은 5번 줄 return으로 끝나 오지 않습니다. '글자가 넘어오면 괜찮다'고 쓰면 검사기가 값을 보고 한 길만 따라간다고 본 것입니다(검사기가 실행을 따라간다는 생각).

다시 볼 레슨: 좁히기 — typeof와 타입 추적 표 · null·undefined 거르기

문제 7

일주일 걸음 수를 km로 바꿉니다. 검사에서 막히는 줄은 몇 번 줄일까요? 고칠 곳은 몇 번 줄일까요?

function km(steps: number) {
  return steps / 1000;
}
const week = ['8000', '12000'];
const first = week[0];
console.log(km(first));
힌트

6번 줄에서 넘기는 first의 타입이 어느 줄에서 정해졌는지 거슬러 갑니다.

정답
검사 결과 오류 1개
main.ts(6,16): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

막히는 줄은 6번, 고칠 곳은 4번 줄입니다. 4번 줄의 배열은 글자를 담아 string[]로 정해지고, 5번 줄에서 꺼낸 first도 string입니다. 그래서 number 매개변수에 넘기는 6번 줄에서 막힙니다. 4번 줄을 const week = [8000, 12000];⁠으로 고치면 8이 찍힙니다.

  • 다르게 고쳐도 되는 곳: 걸음 수가 바깥에서 글자로 온 값이라면 6번 줄에서 km(Number(first))로 바꿔 넘깁니다.

흔한 오답은 고칠 곳을 6번 줄로 쓴 것입니다. 오류가 난 줄이 고칠 줄이라고 본 것입니다(오류 줄이 고칠 줄이라는 생각). 오류의 자리는 발견한 곳이고, 고칠 곳은 그 타입이 정해진 줄입니다.

다시 볼 레슨: 타입 오류 읽기 · 배열과 튜플

문제 8

상품권을 현금처럼 쓰는 함수입니다. 검사 결과와 실행 결과를 각각 쓰세요.

interface Cash {
  won: number;
}
interface Gift {
  won: number;
  until: string;
}
function use(c: Cash) {
  console.log(c.won + '원');
}
const g: Gift = {
  won: 300,
  until: '12월',
};
use(g);
힌트

15번 줄에서 use에 넘기는 g와, use가 받는 자리의 타입을 찾아 둡니다.

힌트 둘

Cash 자리가 요구하는 속성 줄을 g가 가졌는지 봅니다.

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

검사는 오류 없이 지나갑니다. Cash 자리가 요구하는 줄은 won 하나이고, g는 그 줄을 가졌습니다. until 줄이 더 있어도 이름 g를 거친 값이라 됩니다. 검사기는 타입 이름(Cash · Gift)이 아니라 모양으로 맞춥니다(검사 규칙 4).

흔한 오답은 15번 줄에서 막힌다고 쓴 것입니다. Gift는 Cash가 아니니 넣을 수 없다고 본 것입니다(이름이 같아야 맞는다는 생각).

다시 볼 레슨: 모양으로 맞추기 · 새로 쓴 객체의 남는 속성

문제 9

잔 수마다 글을 찍는 함수를 forEach에 넘깁니다. 검사 결과와 실행 결과를 각각 쓰세요.

function show(n: number) {
  console.log(n + '잔');
}
const cups = [2, 3];
cups.forEach(show);
cups.forEach(show(1));
힌트

5번 줄과 6번 줄에서 forEach의 괄호 안에 쓴 것을 견줘 봅니다.

힌트 둘

6번 줄의 show(1)이 언제 불리고 무엇을 돌려주는지 봅니다.

정답
검사 결과 오류 1개
main.ts(6,14): error TS2345: Argument of type 'void' is not assignable to parameter of type '(value: number, index: number, array: number[]) => void'.
실행 결과
2잔3잔1잔main.ts:6cups.forEach(show(1));     ^ TypeError: undefined is not a function    at Array.forEach (<anonymous>)    at main.ts:6:6

검사는 6번 줄에서 막힙니다. show(1)은 그 자리에서 불려 돌려준 값(void — 돌려주는 값 없음)을 넘기는데, forEach는 함수를 받는 자리입니다. Node는 5번 줄에서 2잔 · 3잔을 찍고, 6번 줄에서 show(1)을 먼저 불러 1잔을 찍은 뒤, 돌려받은 undefined를 함수로 부르려다 멈춥니다.

흔한 오답은 실행 결과로 2잔 · 3잔을 두 번 쓴 것입니다. 괄호를 붙여 넘겨도 함수가 넘어간다고 본 것입니다. 함수를 넘길 때는 5번 줄처럼 이름만 씁니다.

JS 다시 보기: 함수를 넘길 때 괄호를 붙이면 그 자리에서 불려, 함수가 아니라 돌려준 값이 넘어갑니다(JS '함수를 넘기기').

다시 볼 레슨: 함수 타입과 콜백

문제 10

컵에 쓸 이름이 있으면 컵: 민지처럼, 없으면 이름 없음을 돌려주는 함수입니다. 두 줄을 찍으면 이렇게 나와야 하고, 검사 오류는 0이어야 합니다.

컵: 민지
이름 없음

아래 줄 ㉠~㉤ 가운데 네 줄을 뼈대의 주석 아래에 놓으세요. 한 줄은 쓰지 않습니다.

㉠   return '이름 없음';
㉡   if (who.length) {
㉢   }
㉣     return '컵: ' + who;
㉤   if (who) {
function cup(who?: string) {
  // 1. 이름이 있으면
  // 2. 없으면
}
console.log(cup('민지'));
console.log(cup());

다 놓았으면 '놓은 뒤 물음'을 펼쳐 하나 더 풉니다.

힌트

'1. 이름이 있으면'은 if 갈래 세 줄, '2. 없으면'은 그 아래 한 줄입니다.

놓은 뒤 물음

쓰지 않는 줄을 빼는 까닭은 무엇일까요? 하나를 고르세요.

  • ① who가 undefined일 수 있어, length를 꺼내는 줄에서 검사가 막히기 때문
  • ② length가 0인 이름도 참으로 쳐서 컵: 만 찍히기 때문
  • ③ if의 조건에는 수를 쓸 수 없기 때문
정답

㉤ ㉣ ㉢ ㉠ 차례이고, 까닭은 ①입니다. 쓰지 않는 줄은 ㉡입니다.

function cup(who?: string) {
  // 1. 이름이 있으면
  if (who) {
    return '컵: ' + who;
  }
  // 2. 없으면
  return '이름 없음';
}
console.log(cup('민지'));
console.log(cup());
실행 결과
컵: 민지
이름 없음

who는 선택 매개변수라 지금 타입이 string | undefined입니다. ㉤의 if (who)는 undefined를 걸러, 갈래 안에서 who가 string으로 좁혀집니다. ㉡은 거르기 전에 length를 꺼내 TS18048(undefined일 수 있다)로 막히고, 실행하면 cup()에서 멈춥니다. 서로 바꿔도 되는 줄은 없습니다.

  • ② length가 0이면 if가 거짓이라 컵: 만 찍히는 일은 없습니다. 걸리는 까닭은 검사입니다.
  • ③ JS의 if는 수도 참 · 거짓으로 봅니다. 수라서가 아닙니다.

다시 볼 레슨: null·undefined 거르기 · 선택·기본값 매개변수

문제 11

쿠폰 코드를 바꾸는 함수입니다. 줄마다 v · out · a · b의 지금 타입을 타입 추적 표로 적고, 검사를 지나갈지 쓰세요. 지나가지 못하면 막히는 줄을 모두 씁니다.

function code(
  v: string | number,
) {
  let out = v;
  if (typeof v === 'number') {
    out = v * 10;
  }
  const a = out;
  out = 'A' + a;
  const b = out.toUpperCase();
  return b + v.length;
}
힌트

표에 적을 줄은 2 · 4 · 5 · 6 · 8 · 9 · 10 · 11번 줄입니다. 6번 줄과 9번 줄은 out에 대입하는 줄입니다.

힌트 둘

대입하는 줄 뒤에는 그 이름의 지금 타입이 선언 타입 가운데 새 값이 드는 쪽으로 좁혀집니다. 5번 줄의 갈래가 끝난 뒤 8번 줄로 오는 길이 몇 개인지 봅니다.

정답

검사는 11번 줄에서 막힙니다.

검사 결과 오류 1개
main.ts(11,16): error TS2339: Property 'length' does not exist on type 'string | number'.  Property 'length' does not exist on type 'number'.
줄이름지금 타입
2vstring | number
4outstring | number(첫 값 v에서 — 선언 타입도 같음)
5vstring | number → 참 number 거짓 string
6vnumber
6out대입 뒤 number
8out만나는 줄 — 참 number + 거짓 string | number → string | number
8astring | number
9out대입 뒤 string
10outstring
10bstring
11v8번 줄에서 합쳐진 그대로 — 참 number + 거짓 string → string | number
  • 8번 줄: 거짓 길의 out은 4번 줄에서 붙은 그대로 string | number라, 합쳐도 string | number입니다.
  • 9 · 10번 줄: 글자에 다시 붙인 뒤라 out은 string이고 toUpperCase가 지나갑니다(대입 뒤 좁힘).
  • 11번 줄: v는 5번 줄 갈래의 두 길이 다시 만난 뒤라 string | number이고, number에는 length가 없어 막힙니다(검사 규칙 5).

흔한 오답 하나는 10번 줄도 막힌다고 쓴 것입니다. 대입해도 지금 타입이 선언 타입 그대로라고 본 것입니다. 다른 하나는 11번 줄의 v를 string으로 보고 '지나감'이라고 쓴 것입니다. 5번 줄의 확인이 11번 줄까지 간다고 본 것입니다(좁힘이 끝까지 간다는 생각).

다시 볼 레슨: 좁힘이 달라지는 곳 — 대입과 반복 · 좁히기 — typeof와 타입 추적 표

문제 12 (만들어 보기)

문구점의 작은 주문표를 혼자 써 봅니다. 공책(note)은 권 수(qty)마다 1500원, 펜(pen)은 펜 값(won) 그대로입니다. 공책 2권과 1200원짜리 펜 하나를 주문했을 때 합계를 찍습니다. 검사 오류는 0이어야 하고, 이렇게 나와야 합니다.

합계 4200원

다 쓰면 주문 목록에 800원짜리 펜 하나를 더해 따라가 봅니다. 합계 5000원이 나와야 합니다.

힌트

하위 목표 다섯(1. 갈래 타입 정하기 · 2. 갈래마다 금액 · 3. 나머지는 never로 · 4. 하나씩 꺼내 더하기 · 5. 쓰기)의 주석을 먼저 쓰고 그 아래를 채웁니다.

힌트 둘

주문 목록에도 타입을 적습니다.

정답

모범 답입니다. 블록 셋을 차례로 이어 씁니다.

// 1. 갈래 타입 정하기
interface Note {
  kind: 'note';
  qty: number;
}
interface Pen {
  kind: 'pen';
  won: number;
}
type Item = Note | Pen;

위 타입 선언에 이어 씁니다.

// 2. 갈래마다 금액
function cost(o: Item) {
  switch (o.kind) {
    case 'note':
      return o.qty * 1500;
    case 'pen':
      return o.won;
    // 3. 나머지는 never로
    default:
      const left: never = o;
      return left;
  }
}
검사 결과 오류 없음

앞 코드에 이어 씁니다.

const items: Item[] = [
  { kind: 'note', qty: 2 },
  { kind: 'pen', won: 1200 },
];
// 4. 하나씩 꺼내 더하기
let sum = 0;
for (const o of items) {
  sum = sum + cost(o);
}
// 5. 쓰기
console.log(`합계 ${sum}원`);
실행 결과
합계 4200원

800원 펜을 더하면 셋째 바퀴에서 sum이 4200에서 5000이 되어 합계 5000원입니다. 손으로 셈한 2 × 1500 + 1200 + 800과 맞춰 봅니다.

  • 다르게 써도 되는 곳: 이름(Note · cost · sum)은 달라도 됩니다. 2번 하위 목표를 if로 써도 같은 화면이 나오지만, never 줄이 없으면 갈래가 늘어도 검사기가 알려 주지 않습니다.
  • 검사 오류가 0이고, 찍은 화면이 위와 같고, 펜을 더한 결과도 맞으면 맞습니다.
  • 흔한 오답 넷(모두 돌려 본 것):
    • 주문 목록에 Item[]을 적지 않으면 kind가 string으로 넓어져, cost에 넘기는 줄이 TS2345로 막힙니다(넓어지지 않는다는 생각).
    • sum = cost(o);로 쓰면 검사는 지나지만 마지막 주문 값만 남아 합계 1200원입니다.
    • 공책 금액을 o.qty + 1500으로 쓰면 검사는 지나지만 합계 2702원입니다. 검사를 지나도 합계는 손으로 맞춰 봅니다(검사를 지나면 맞다는 생각).
    • case 'pen'을 빠뜨리면 never 줄이 TS2322로 짚습니다.

다시 볼 레슨: 작은 주문표 만들기 · 빠짐없이 — never로 갈래 검사 · 판별 유니언

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

타입스크립트 목차로