하루 지나 풀면 더 오래 남습니다. 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가 각각 어떻게 다루는지 따로 봅니다.
정답
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번 줄에서 막힙니다.
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번 줄에서 막힙니다.
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번 줄에서 막힙니다.
main.ts(13,10): error TS18046: 'x' is of type 'unknown'.| 줄 | 이름 | 지금 타입 |
|---|---|---|
| 6 | x | unknown |
| 7 | x | unknown → 참 number · 거짓 unknown |
| 8 | x | number |
| 10 | x | 만나는 줄 — 7번 줄 참(return 없음) + 거짓 → unknown |
| 10 | x | unknown → 참 string · 거짓 unknown |
| 11 | x | string (return으로 끝남) |
| 13 | x | 10번 줄 거짓 길 → 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번 줄에서 막힙니다.
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번 줄에서 막힙니다.
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을 어떻게 다루는지 봅니다.
정답
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이 실행될 때의 값을 따로 봅니다.
정답
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번 줄 두 군데에서 막힙니다.
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 클래스를 혼자 써 봅니다.
summary(): string메서드를 요구하는interface Printable을 선언합니다.- 적립(
kind: 'earn',count: number)과 사용(kind: 'use',count: number) 두 갈래를 묶은type Action을 선언합니다. 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를 지키는 클래스 · 판별 유니언