본문으로 건너뛰기

단원 1~10 섞어 풀기

타입스크립트 목차모듈·선언·설정문제 12개

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

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

긴 문제 둘(6 · 11)은 타입 추적 표를 끝까지 그려 풉니다. 지금 타입은 유니언의 차례와 리터럴의 따옴표 모양이 정답과 달라도 같은 답입니다. 마지막 문제는 클래스와 판별 유니언을 함께 쓰는 작은 주문표를 혼자 써 봅니다. 타입 선언이 긴 코드는 블록을 나눴고, 둘째 블록부터는 앞 블록에 이어 쓴 것이라 줄 번호도 이어서 셉니다. 검사 결과는 tsc 7.0.2로, 실행 결과는 Node 26으로 돌린 것입니다.

문제 1

도장 카드의 stamps를 private으로 선언했습니다. 검사 결과와 실행 결과를 각각 쓰세요.

class StampCard {
  private stamps: number = 2;
}
const c = new StampCard();
console.log(c.stamps + 1);
힌트

2번 줄의 private을 검사기와 Node가 각각 어떻게 다루는지 따로 봅니다.

정답
검사 결과 오류 1개
main.ts(5,15): error TS2341: Property 'stamps' is private and only accessible within class 'StampCard'.
실행 결과
3

검사는 5번 줄에서 막힙니다. private 속성을 클래스 밖에서 읽으려 했습니다. Node는 private과 : number를 지우고 실행하므로 c.stamps는 일반 속성 2가 되어 3을 찍습니다(검사 규칙 1). 실행 때까지 숨기려면 #stamps를 씁니다.

흔한 오답 하나는 실행 결과를 비우거나 오류로 멈춘다고 쓴 것입니다(타입이 실행 때 남는다는 생각).

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

문제 2

바깥에서 온 가격 JSON 글자를 as로 단언해 더합니다. 검사 결과와 실행 결과를 각각 쓰세요.

interface Row {
  price: number;
}
const s = '{"price":"4500"}';
const r =
  JSON.parse(s) as Row;
console.log(r.price + 500);
힌트

5~6번 줄의 as Row가 검사기에게 하는 말과, 4번 줄의 "4500"이 실행 때 어떤 값인지 따로 봅니다.

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

검사는 오류 없이 지나갑니다. as Row가 검사기에게 r.price를 number라고 믿게 했습니다. 실행할 때는 as Row가 지워지고 원래 글자 '4500'에 500이 이어 붙어 4500500이 찍힙니다(검사 규칙 1 · 6).

흔한 오답 하나는 5000이라고 쓴 것입니다. as Row가 '4500'을 수로 바꾼다고 본 것입니다(단언이 값을 바꾼다는 생각).

다시 볼 레슨: as는 검사기에게 하는 말 · 바깥 값 확인하기

문제 3

Adder 인터페이스를 지키는 Counter 클래스입니다. 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.

interface Adder {
  add(n: number): number;
}
class Counter implements Adder {
  total: number = 0;
  add(n): number {
    this.total += n;
    return this.total;
  }
}
힌트

6번 줄의 매개변수 n에 타입 표기가 있는지 봅니다.

힌트 둘

implements는 클래스 모양을 대조만 할 뿐 매개변수 타입을 추론해 주지 않습니다.

정답

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

검사 결과 오류 1개
main.ts(6,7): error TS7006: Parameter 'n' implicitly has an 'any' type.

implements Adder를 적었더라도 클래스 메서드의 매개변수 타입(add(n: number))은 직접 적어야 합니다.

흔한 오답 하나는 '지나감'이라고 쓴 것입니다. 인터페이스에 적힌 타입을 클래스 메서드가 물려받아 추론한다고 본 것입니다(implements가 매개변수 타입을 추론한다는 생각).

다시 볼 레슨: interface를 지키는 클래스 · strict가 켜는 것들

문제 4

하고 싶은 일마다 쓸 키워드나 타입 이름을 쓰세요.

  • (가) 다른 파일에서 실행 전에 통째로 지워질 타입만 가져올 때 import 뒤에 붙이는 낱말
  • (나) 실행 코드 없이 타입만 적은 선언 파일(.d.ts)에서 외부 전역 값의 타입을 알릴 때 쓰는 낱말
  • (다) 객체 타입 Menu의 모든 속성 이름을 리터럴 유니언("item" | "price")으로 뽑는 연산자
  • (라) 객체 타입 Menu에서 일부 속성("price")만 골라 새 객체 타입을 만드는 내장 제네릭 유틸리티 타입
힌트

(가)는 verbatimModuleSyntax가 요구하는 표기이고, (다)·(라)는 8단원에서 배운 타입 도구입니다.

정답

받는 답: (가) type(import type) · (나) declare · (다) keyof · (라) Pick

다시 볼 레슨: 모듈과 타입 내보내기 · 선언 파일과 @types · keyof와 속성 타입 꺼내기 · 있는 타입에서 새 타입 — Pick·Omit·Partial

문제 5

브라우저에서 수량 입력 칸을 찾아 읽는 코드입니다. 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.

const el = document
  .querySelector('#qty');
if (el !== null) {
  console.log(el.value);
}
힌트

3번 줄 el !== null을 지난 뒤 4번 줄에서 el의 지금 타입이 무엇인지 봅니다.

정답

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

검사 결과 오류 1개
main.ts(4,18): error TS2339: Property 'value' does not exist on type 'Element'.

el !== null은 null만 거를 뿐 el의 지금 타입은 여전히 Element입니다. Element에는 value가 없으므로 el instanceof HTMLInputElement⁠로 좁혀야 합니다.

다시 볼 레슨: DOM 요소의 타입

문제 6

바깥에서 온 값 x를 확인해 처리하는 함수입니다. 줄마다 x의 지금 타입을 타입 추적 표로 적고, 검사를 지나갈지 쓰세요. 지나가지 못하면 막히는 줄을 모두 씁니다.

function isNum(
  v: unknown,
): v is number {
  return typeof v === 'number';
}
function run(x: unknown) {
  if (isNum(x)) {
    console.log(x.toFixed(0));
  }
  if (typeof x === 'string') {
    return x.trim();
  }
  return x.toString();
}
힌트

7번 줄 갈래와 10번 줄 갈래가 각각 return으로 끝나는지 보고, 13번 줄에 도달하는 길을 따져 봅니다.

정답

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

검사 결과 오류 1개
main.ts(13,10): error TS18046: 'x' is of type 'unknown'.
줄이름지금 타입
6xunknown
7xunknown → 참 number · 거짓 unknown
8xnumber
10x만나는 줄 — 7번 줄 참(return 없음) + 거짓 → unknown
10xunknown → 참 string · 거짓 unknown
11xstring (return으로 끝남)
13x10번 줄 거짓 길 → unknown (TS18046 막힘)

7번 줄의 참 갈래는 return이 없어 10번 줄에서 다시 unknown이 되고, 좁히지 않은 13번 줄에서는 메서드를 부를 수 없습니다(검사 규칙 5).

흔한 오답 하나는 '지나감'이라고 쓴 것입니다. 7 · 10번 줄의 좁힘이 끝까지 간다고 본 것입니다(좁힘이 끝까지 간다는 생각).

다시 볼 레슨: unknown — 확인하고 쓰기 · 직접 만드는 확인 함수 · 좁히기 — typeof와 타입 추적 표

문제 7

객체에서 지정한 키의 값을 꺼내는 제네릭 함수입니다. 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.

function get<
  T,
  K extends keyof T,
>(obj: T, key: K): T[K] {
  return obj[key];
}
const menu = {
  item: '라테',
  price: 4500,
};
const a = get(menu, 'price');
const b = get(menu, 'size');
힌트

keyof typeof menu에 어떤 키 이름들이 들어 있는지 11 · 12번 줄과 견줘 봅니다.

정답

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

검사 결과 오류 1개
main.ts(12,21): error TS2345: Argument of type '"size"' is not assignable to parameter of type '"item" | "price"'.

menu의 키 모음(keyof T)은 'item' | 'price'이므로, 없는 속성 이름 'size'를 넘긴 12번 줄이 TS2345로 막힙니다. 11번 줄의 a는 number로 추론됩니다.

흔한 오답 하나는 '지나감'이라고 쓴 것입니다. 제네릭 칸 K에는 아무 문자열이나 들어간다고 본 것입니다(T는 아무거나라는 생각).

다시 볼 레슨: 제약 — extends로 칸에 조건 달기 · keyof와 속성 타입 꺼내기

문제 8

주차 정산 앱이 주차 시간으로 요금을 구해 할인액 1000원을 뺍니다. 6번 줄에서 fee의 지금 타입과 이 코드의 검사 결과를 쓰세요.

async function feeOf(
  h: number,
) {
  return h * 2000;
}
const fee = feeOf(3);
console.log(fee - 1000);
힌트

async 함수가 돌려주는 것을 await 없이 바로 받으면 6번 줄에서 fee의 지금 타입이 무엇인지 봅니다.

정답

6번 줄에서 fee의 지금 타입은 Promise<number>이고, 검사는 7번 줄에서 막힙니다.

검사 결과 오류 1개
main.ts(7,13): error TS2362: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.

async 함수는 항상 Promise<T> 상자를 돌려주므로, await feeOf(3)으로 열기 전에는 fee가 number가 아니라 Promise<number>입니다. 따라서 7번 줄의 뺄셈 왼쪽 자리에 쓸 수 없어 TS2362로 막힙니다.

흔한 오답 하나는 fee를 number로 쓰고 '지나감'이라고 쓴 것입니다(async 함수가 값을 돌려준다는 생각).

다시 볼 레슨: Promise와 async의 타입

문제 9

다음 코드를 과목 기본 설정으로 검사하고 실행할 때의 결과를 각각 쓰세요.

enum Grade {
  Silver,
  Gold,
}
console.log('완료');
힌트

과목 tsconfig.json의 erasableSyntaxOnly와 Node의 타입 지우기 전용 실행이 enum을 어떻게 다루는지 봅니다.

정답
검사 결과 오류 1개
main.ts(1,6): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
실행 결과
main.ts:1  > enum Grade {      Silver,      Gold,  > }    console.log('완료'); SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode {  code: 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX'}

enum은 타입만 지워서는 실행할 수 없고 객체를 새로 만들어야 하는 문법이라, tsc에서는 TS1294로 막히고 Node 직접 실행에서도 문법 오류로 멈춥니다. 대신 type Grade = 'silver' | 'gold' 리터럴 유니언을 씁니다.

다시 볼 레슨: tsconfig.json 읽기 · 리터럴 유니언으로 값 고르기

문제 10

다음 코드를 과목 기본 설정("strict": true)으로 검사하고 실행할 때의 결과를 각각 쓰세요.

function addTip(price) {
  return price + 500;
}
console.log(addTip('4000'));
힌트

1번 줄 매개변수 price의 타입 표기 유무와, 4번 줄에서 '4000' + 500이 실행될 때의 값을 따로 봅니다.

정답
검사 결과 오류 1개
main.ts(1,17): error TS7006: Parameter 'price' implicitly has an 'any' type.
실행 결과
4000500

검사기는 1번 줄에서 noImplicitAny 규칙으로 TS7006을 내지만, price가 any가 되어 4번 줄 호출은 막지 못합니다. Node는 그대로 실행해 문자열 '4000500'을 찍습니다(검사 규칙 1). 1번 줄에 price: number를 적으면 4번 줄의 잘못된 인자 '4000'까지 TS2345로 잡아 줍니다.

흔한 오답 하나는 실행 결과를 비운 것입니다. 1번 줄의 검사 오류 때문에 Node가 돌지 않는다고 본 것입니다(검사 오류가 실행을 막는다는 생각).

다시 볼 레슨: strict가 켜는 것들 · any는 검사를 끈다

문제 11

클래스와 판별 유니언을 함께 쓴 코드입니다. 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.

interface Cash {
  kind: 'cash';
  amount: number;
}
interface Coupon {
  kind: 'coupon';
  discount: number;
}
type Pay = Cash | Coupon;
class Wallet {
  balance: number;
  apply(p: Pay): number {
    if (p.kind === 'cash') {
      return p.amount;
    }
    return p.amount;
  }
}
힌트

11번 줄의 속성 balance가 초기화되었는지, 그리고 16번 줄에서 p의 지금 타입이 무엇인지 두 군데를 모두 봅니다.

정답

검사는 11번 줄과 16번 줄 두 군데에서 막힙니다.

검사 결과 오류 2개
main.ts(11,3): error TS2564: Property 'balance' has no initializer and is not definitely assigned in the constructor.main.ts(16,14): error TS2339: Property 'amount' does not exist on type 'Coupon'.
  • 11번 줄: balance: number;에 초기값(= 0)도 없고 constructor 대입도 없어 TS2564로 막힙니다.
  • 16번 줄: 13번 줄 if (p.kind === 'cash')⁠가 return으로 끝났으므로 16번 줄에서 p의 지금 타입은 Coupon으로 좁혀져 있습니다. Coupon에는 amount가 없고 discount만 있어 TS2339로 막힙니다.

다시 볼 레슨: 클래스에 타입 붙이기 · 판별 유니언

문제 12 (만들어 보기)

도장 적립과 사용을 처리하는 StampCard 클래스를 혼자 써 봅니다.

  1. summary(): string 메서드를 요구하는 interface Printable을 선언합니다.
  2. 적립(kind: 'earn', count: number)과 사용(kind: 'use', count: number) 두 갈래를 묶은 type Action을 선언합니다.
  3. implements Printable을 붙인 class StampCard를 만들고, stamps: number = 0, 갈래마다 도장을 더하거나 빼는 run(a: Action): void, 그리고 'N개'를 돌려주는 summary(): string을 작성합니다.

카드를 만들어 5개 적립 후 2개 사용한 뒤 summary()를 찍으면 검사 오류는 0이어야 하고 아래와 같이 나와야 합니다.

3개
힌트

a.kind === 'earn'으로 갈래를 나누어 증감할 개수를 계산하고 this.stamps에 더합니다.

정답

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

interface Printable {
  summary(): string;
}
interface Earn {
  kind: 'earn';
  count: number;
}
interface Use {
  kind: 'use';
  count: number;
}
type Action = Earn | Use;

갈래마다 증감값을 돌려주는 함수를 이어 씁니다.

function delta(a: Action) {
  return a.kind === 'earn'
    ? a.count
    : -a.count;
}

위 선언에 이어 클래스를 작성합니다.

class StampCard
  implements Printable
{
  stamps: number = 0;
  run(a: Action): void {
    this.stamps += delta(a);
  }
  summary(): string {
    const n = this.stamps;
    return `${n}개`;
  }
}

이어서 인스턴스를 만들고 실행합니다.

const card = new StampCard();
card.run({
  kind: 'earn',
  count: 5,
});
card.run({
  kind: 'use',
  count: 2,
});
console.log(card.summary());
실행 결과
3개

검사 오류가 0이고 3개가 찍히면 맞습니다.

다시 볼 레슨: 클래스에 타입 붙이기 · interface를 지키는 클래스 · 판별 유니언

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

타입스크립트 목차로