8단원 '타입으로 타입 만들기'의 다섯 레슨에서 아홉 문제를 내고, 앞 단원(4 · 6 · 7단원)에서 세 문제를 섞었습니다. 앞 단원 문제가 어느 레슨인지는 정답을 펼치면 보입니다. 검사 결과는 지나가는지와 막히는 줄을, 지금 타입은 검사기가 찍는 글자를, 실행 결과는 찍히는 글자를 종이에 끝까지 쓴 뒤 정답을 펼치세요. 실행이 오류로 멈추면 오류 이름이 든 줄까지만 써도 됩니다. 막히면 정답 앞의 힌트를 먼저 펼칩니다. 점수는 매기지 않습니다.
한 번에 다 풀 필요는 없습니다. 문제 1~10은 폰으로도 풀 수 있고(약 30~40분), 끝의 만들어 보기 둘은 컴퓨터에서 tsc와 Node로 직접 검사하고 돌려 봅니다. 이 단원을 건너뛸지 보려면 진단 문항인 문제 1 · 3 · 5 · 7 · 9 다섯을 먼저 풀어 보세요(약 15분). 진단 문항은 힌트를 펴지 않고 풉니다. 힌트를 펴고 맞힌 문제나 짐작으로 맞힌 문제는 틀린 것으로 칩니다. 모두 맞으면 클래스에 타입 붙이기로 넘어가고, 틀린 문제가 있으면 그 정답 끝의 레슨만 봅니다. 끝의 만들어 보기 둘은 골라서 풉니다.
타입 선언이 긴 문제와 만들어 보기는 코드를 블록 여럿으로 나눴습니다. 둘째 블록부터는 앞 블록에 이어 쓴 것이고 줄 번호도 이어서 셉니다. 끝의 두 문제는 있는 타입에서 새 타입을 뽑아 고치기 함수를 직접 만들어 봅니다. 컴퓨터가 있으면 블록을 차례로 main.ts에 이어 붙여 npx tsc로 검사 오류가 0인지 본 뒤 node main.ts로 돌려 맞춰 봅니다. 플레이그라운드에서는 맨 위에 export {}; 한 줄을 더합니다. 폰으로만 본다면 문제에 적힌 값으로 내 코드를 한 줄씩 검사하고 따라가 나와야 할 화면과 맞춰 봅니다. 검사 결과는 tsc 7.0.2로, 실행 결과는 Node 26으로 돌린 것입니다.
문제 1
도서관 앱이 책 타입 Book에 대출 일수를 더한 Loan과 분류표를 더한 Tagged를 만들었습니다.
interface Book {
title: string;
}
interface Loan extends Book {
days: number;
}
type Tagged = Book & {
tag: string;
};
위 타입 선언에 이어 씁니다. 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.
const a: Loan = {
title: '모모',
};
const b: Tagged = {
title: '해변',
};
const c: Loan = {
title: '숲',
days: 7,
};
const d: Book = c;
힌트
10 · 13 · 16 · 20번 줄마다 왼쪽 타입이 요구하는 속성 줄을 오른쪽 값이 모두 가졌는지 봅니다.
힌트 둘
extends와 &는 모두 두 모양의 속성 줄을 합칩니다. 20번 줄은 이름 c를 거친 값을 Book 자리에 넣습니다.
정답
검사는 10번 줄과 13번 줄에서 막힙니다.
main.ts(10,7): error TS2741: Property 'days' is missing in type '{ title: string; }' but required in type 'Loan'.main.ts(13,7): error TS2322: Type '{ title: string; }' is not assignable to type 'Tagged'. Property 'tag' is missing in type '{ title: string; }' but required in type '{ tag: string; }'.- 10번 줄:
Loan은Book의title에days줄을 더한 모양이라,days가 빠져 막힙니다. - 13번 줄:
Book & { tag: string }은 두 모음이 겹치는 곳이라 두 모양의 줄(title·tag)이 모두 있어야 합니다.tag가 빠져 막힙니다. - 20번 줄:
c는Book이 요구하는title줄을 가졌고 이름c를 거친 값이라,days줄이 더 있어도 지나갑니다(검사 규칙 4).
흔한 오답 하나는 13번 줄이 지나간다고 쓴 것입니다. &를 유니언(|)처럼 둘 가운데 한쪽 모양만 있어도 되는 것으로 본 것입니다. 20번 줄도 막힌다고 썼다면 Loan과 Book은 이름이 달라 넣을 수 없다고 본 것이거나(이름이 같아야 맞는다는 생각), days가 남아서 막힌다고 본 것입니다(남는 속성은 늘 오류라는 생각).
다시 볼 레슨: 타입에 줄 더하기 — extends와 & · 모양으로 맞추기
문제 2
카페 앱이 바깥에서 온 값이 쿠폰(Coupon)인지 확인하는 함수 isCoupon을 만들었습니다.
interface Coupon {
code: string;
off: number;
}
function isCoupon(
v: unknown,
): v is Coupon {
return (
typeof v === 'object' &&
v !== null &&
'code' in v &&
typeof v.code === 'string'
);
}
위 선언에 이어 씁니다. 12번 줄에서 v의 지금 타입과 18번 줄에서 raw의 지금 타입은 각각 무엇일까요? 검사 결과와 실행 결과도 각각 쓰세요.
const raw =
JSON.parse('{"code":"봄"}');
if (isCoupon(raw)) {
console.log(raw.off + 500);
}
힌트
9~11번 줄의 확인 셋을 지난 뒤 12번 줄의 v에 무엇이 더해졌는지 봅니다. 17번 줄의 isCoupon(raw)가 참일 때 검사기가 무엇을 믿는지도 봅니다.
힌트 둘
'code' in v를 지나면 object에 Record<"code", unknown>이 &로 합쳐집니다. 밖에서는 검사기가 v is Coupon이라는 말을 그대로 믿습니다.
정답
12번 줄에서 v의 지금 타입은 object와 Record<"code", unknown>을 &로 합친 타입(아래 표의 12번 줄)이고, 18번 줄에서 raw의 지금 타입은 Coupon입니다. 검사는 오류 없이 지나가고, 실행하면 NaN이 찍힙니다.
NaN| 줄 | 이름 | 지금 타입 |
|---|---|---|
| 6 | v | unknown |
| 12 | v | object & Record<"code", unknown> |
| 18 | raw | Coupon |
9~11번 줄에서 객체이고 null이 아니며 'code' 속성이 있다는 것을 확인해, 12번 줄의 v는 object와 Record<"code", unknown>을 &로 합친 타입이 됩니다. 밖에서는 검사기가 v is Coupon이라는 반환 타입을 믿어 18번 줄의 raw를 Coupon으로 좁힙니다(검사 규칙 5 · 6). 하지만 isCoupon이 off 속성은 확인하지 않아, 실행 때 raw.off는 undefined이고 undefined + 500이 되어 NaN이 찍힙니다.
같은 답으로 치는 꼴: 12번 줄의 v 타입에서 "code"를 'code'처럼 작은따옴표로 쓴 것.
흔한 오답 하나는 isCoupon이 off를 확인하지 않았으니 18번 줄이 검사에서 막힌다고 본 것입니다. v is Coupon만 적으면 검사기가 함수 안까지 대조해 확인해 준다고 본 것입니다(단언이 값을 바꾼다는 생각).
JS 다시 보기: 없는 속성을 읽으면 undefined이고, undefined + 500은 NaN입니다(JS '객체: 이름 붙은 칸 묶음').
다시 볼 레슨: 직접 만드는 확인 함수 · 타입에 줄 더하기 — extends와 & · Record — 키 타입으로 만드는 객체 타입
문제 3
주차장 요금표 앱이 기본 요금 base와 시간 단위 unit을 다룹니다.
interface Rate {
base: number;
unit: 'min' | 'hour';
}
function read(
r: Rate,
k: keyof Rate,
) {
return r[k];
}
위 선언에 이어 씁니다. 7번 줄에서 매개변수 k의 지금 타입(풀어 쓴 유니언으로)과 15번 줄에서 u의 지금 타입은 각각 무엇일까요? 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.
const r: Rate = {
base: 1000,
unit: 'min',
};
const u: Rate['unit'] = r.unit;
console.log(read(r, 'max'));
console.log(Rate['base']);
힌트
Rate의 속성 이름이 무엇인지 보고, 15번 줄의 Rate['unit']과 17번 줄의 Rate['base']가 각각 타입 자리에 있는지 값 자리에 있는지 봅니다.
힌트 둘
keyof Rate는 Rate의 속성 이름들을 모은 리터럴 유니언입니다. Rate['base']는 타입을 꺼내는 문법이라 콜론 뒤 같은 타입 자리에만 쓸 수 있습니다.
정답
7번 줄에서 k의 지금 타입은 "base" | "unit"(keyof Rate), 15번 줄에서 u의 지금 타입은 "hour" | "min"이고, 검사는 16번 줄과 17번 줄에서 막힙니다.
main.ts(16,21): error TS2345: Argument of type '"max"' is not assignable to parameter of type 'keyof Rate'.main.ts(17,13): error TS2693: 'Rate' only refers to a type, but is being used as a value here.- 7번 줄:
keyof Rate는Rate의 속성 이름을 모은"base" | "unit"입니다(검사기가 찍는 이름 붙은 꼴은keyof Rate). - 15번 줄:
Rate['unit']은unit속성의 타입을 꺼낸"hour" | "min"입니다.r의 선언 타입이Rate라r.unit도"hour" | "min"입니다. - 16번 줄:
'max'는"base" | "unit"에 들지 않아 TS2345로 막힙니다. - 17번 줄:
console.log(...)안은 값 자리인데 타입인Rate를 써서 TS2693(only refers to a type— 타입만 가리키는데 값으로 썼다)으로 막힙니다. 값을 꺼내려면r['base']를 씁니다.
같은 답으로 치는 꼴: 7번 줄을 keyof Rate로 쓴 것, 유니언 차례나 작은따옴표만 다르게 쓴 것('base' | 'unit', 'min' | 'hour').
흔한 오답 하나는 17번 줄이 지나간다고 쓴 것입니다. 타입 자리의 Rate['base']와 값 자리의 r['base']를 섞어 본 것입니다(타입과 값을 섞는 생각).
다시 볼 레슨: keyof와 속성 타입 꺼내기
문제 4
음악 앱이 곡 객체에서 지정한 속성을 꺼내 10초를 더합니다. 210이 찍혀야 하는데, 검사에서 막히고 돌리면 NaN이 찍힙니다.
interface Song {
id: number;
sec: number;
}
function get<
T,
K extends keyof T,
>(x: T, k: K) {
return x[k];
}
위 선언에 이어 쓴 코드와 검사 · 실행 결과입니다.
const s: Song = {
id: 1,
sec: 200,
};
const v = get(s, 'secs');
console.log(v + 10);
main.ts(15,18): error TS2345: Argument of type '"secs"' is not assignable to parameter of type 'keyof Song'.NaN아래 고치기 가운데 검사 오류를 없애는 것을 모두 고르세요. 그 가운데 실행 결과까지 210이 되는 것도 모두 고르세요.
- ① 15번 줄을
const v = get(s, 'sec');으로 고친다 - ② 15번 줄 앞에
// @ts-ignore한 줄을 더한다 - ③ 15번 줄을
const v = get(s, 'secs' as any);로 고친다 - ④ 7번 줄을
K extends string,으로 고친다
힌트
15번 줄에서 넘기는 'secs'가 Song의 속성 이름과 맞는지 보고, 보기마다 9번 줄과 15번 줄의 검사와 실행 값을 따라가 봅니다.
힌트 둘
K extends keyof T는 T에 실제로 있는 키만 받습니다. // @ts-ignore와 as any는 오류만 가리고 값은 바꾸지 않습니다.
정답
검사 오류를 없애는 것은 ①②③이고, 실행 결과까지 210이 되는 것은 ①뿐입니다.
K extends keyof T 조건 덕에 검사기가 Song에 없는 오타 'secs'를 15번 줄에서 잡아냈습니다(검사 규칙 7). ①처럼 키 이름을 'sec'으로 바로잡으면 v가 200이 되어 210을 찍습니다.
- ②와 ③은 15번 줄의 검사 오류만 덮습니다. 실행 때
s['secs']는undefined라 그대로NaN이 찍힙니다. - ④는 15번 줄 오류는 사라지지만 9번 줄의
return x[k];가 TS2536(Type 'K' cannot be used to index type 'T')으로 막힙니다. 아무 문자열이나T의 키로 쓸 수는 없기 때문입니다.
흔한 오답 하나는 ②나 ③도 210을 찍는다고 고른 것입니다(검사를 지나면 맞다는 생각). ④가 검사를 지난다고 골랐다면 제네릭 T에서 아무 문자열 키나 꺼낼 수 있다고 본 것입니다(T는 아무거나라는 생각).
다시 볼 레슨: keyof와 속성 타입 꺼내기 · 제약 — extends로 칸에 조건 달기
문제 5
카페 앱이 사이즈 배열 sizes와 온도 배열 temps에서 각각 원소 타입을 뽑아 썼습니다. 12번 줄에서 s와 t의 지금 타입은 각각 무엇일까요? 검사 결과와 실행 결과도 각각 쓰세요. 실행 결과는 마지막 줄까지 글자 그대로 씁니다.
const sizes = [
'tall',
'grande',
] as const;
const temps = ['hot', 'iced'];
type Size =
(typeof sizes)[number];
type Temp =
(typeof temps)[number];
const s: Size = 'short';
const t: Temp = 'warm';
console.log(s, t);
힌트
4번 줄의 sizes에는 as const가 붙어 있고, 5번 줄의 temps에는 붙어 있지 않은 것을 견줘 봅니다.
힌트 둘
as const가 붙은 배열에서 [number]로 뽑으면 리터럴 유니언이 되고, as const가 없는 배열은 string[]으로 넓어져 [number]도 string이 됩니다. 타입과 as const는 실행 전에 지워집니다.
정답
12번 줄에서 s의 지금 타입은 Size("grande" | "tall"), t는 string이고, 검사는 10번 줄에서 막힙니다. 실행하면 short warm이 찍힙니다.
main.ts(10,7): error TS2322: Type '"short"' is not assignable to type '"grande" | "tall"'.short warm| 줄 | 이름 | 지금 타입 |
|---|---|---|
| 1 | sizes | readonly ["tall", "grande"] |
| 5 | temps | string[] |
| 12 | s | Size("grande" | "tall") |
| 12 | t | string |
sizes는 as const로 좁혀 추론되어 (typeof sizes)[number]가 "grande" | "tall"이 됩니다. 그래서 10번 줄의 'short'는 막힙니다. 반면 temps는 as const가 없어 string[]으로 넓어지고(검사 규칙 3), Temp가 string이라 11번 줄의 'warm'은 검사를 지나갑니다. 실행 때는 타입과 as const가 모두 지워져 short warm이 찍힙니다(검사 규칙 1).
같은 답으로 치는 꼴: s를 "grande" | "tall"이나 "tall" | "grande"(또는 작은따옴표)로 풀어 쓴 것, t를 Temp로 쓴 것.
흔한 오답 하나는 11번 줄도 막힌다고 쓴 것입니다. as const 없이 만든 배열도 원소 리터럴 타입을 유지한다고 본 것입니다(넓어지지 않는다는 생각). 실행 결과를 비웠다면 검사 오류가 실행을 막는다고 본 것이고(검사 오류가 실행을 막는다는 생각), as const를 값을 바꾸거나 덮는 단언과 같다고 보면 sizes의 역할을 놓칩니다(단언이 값을 바꾼다는 생각).
다시 볼 레슨: 값에서 타입 뽑기 — typeof와 as const · 리터럴 유니언으로 값 고르기 · 타입은 실행 전에 지워진다
문제 6
배송 앱이 상자 샘플 box에서 타입 Box를 뽑고, 송장 번호나 무게를 글자로 다듬습니다. 12번 줄에서 v의 지금 타입은 무엇일까요? 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.
const box = {
code: 'B1',
kg: 3,
};
type Box = typeof box;
function label(
v: Box['code'] | Box['kg'],
) {
if (typeof v === 'number') {
console.log(v.toFixed(1));
}
return v.toUpperCase();
}
힌트
5번 줄의 typeof box와 9번 줄의 typeof v가 각각 무엇을 하는지 가르고, 9번 줄 갈래가 return으로 끝나는지 봅니다.
힌트 둘
5번 줄은 타입 자리에서 box의 모양 { code: string; kg: number; }를 뽑아 7번 줄의 v가 string | number가 됩니다. 9번 줄의 참 갈래는 return이 없어 12번 줄로 두 길이 모두 옵니다.
정답
12번 줄에서 v의 지금 타입은 string | number이고, 검사는 12번 줄에서 막힙니다.
main.ts(12,12): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.| 줄 | 이름 | 지금 타입 |
|---|---|---|
| 7 | v | string | number |
| 9 | v | string | number → 참 number 거짓 string |
| 10 | v | number |
| 12 | v | 만나는 줄 — 참 number + 거짓 string → string | number |
5번 줄의 typeof box는 타입 자리에서 값 box의 모양 { code: string; kg: number; }를 뽑습니다. 그래서 Box['code'] | Box['kg']는 string | number입니다. 9번 줄의 typeof v === 'number'는 값 자리에서 실행 때 종류를 확인해 갈래 안의 v를 number로 좁히지만, return 없이 끝나 12번 줄에서 참 길(number)과 거짓 길(string)이 다시 합쳐집니다(검사 규칙 5).
같은 답으로 치는 꼴: number | string처럼 차례만 바꿔 쓴 것.
흔한 오답 하나는 12번 줄의 v를 string으로 쓰고 '지나감'이라고 쓴 것입니다. 9번 줄에서 수를 처리했으니 그 아래는 글자만 남는다고 본 것입니다(좁힘이 끝까지 간다는 생각). 5번 줄에서 막힌다고 썼다면 타입 자리의 typeof를 실행 때 글자를 내는 연산자와 섞어 본 것입니다(타입과 값을 섞는 생각).
다시 볼 레슨: 좁히기 — typeof와 타입 추적 표 · 값에서 타입 뽑기 — typeof와 as const · keyof와 속성 타입 꺼내기
문제 7
카페 앱이 메뉴 타입 Menu 하나에서, 새로 등록할 때 쓰는 Draft(id만 뺀 타입)와 일부만 고칠 때 쓰는 Patch(모든 속성을 선택으로 바꾼 타입)를 만듭니다. 이렇게 나와야 합니다.
라테 4800
검사를 지나고 실행 결과가 위처럼 되도록 6번 줄과 7번 줄의 빈칸에 알맞은 내장 타입 이름을 채우세요. any로 덮은 답은 받지 않습니다.
interface Menu {
id: number;
item: string;
price: number;
}
type Draft = ____<Menu, 'id'>;
type Patch = ____<Menu>;
const d: Draft = {
item: '라테',
price: 4500,
};
const p: Patch = {
price: 4800,
};
console.log(d.item, p.price);
힌트
8~11번 줄의 d에는 id가 없고 item · price만 있으며, 12~14번 줄의 p에는 price 하나만 있는 것을 봅니다.
힌트 둘
원래 타입에서 지정한 속성을 뺀 새 타입은 Omit, 모든 속성을 선택 속성(?)으로 바꾼 새 타입은 Partial입니다.
정답
받는 답: 6번 줄 Omit · 7번 줄 Partial
6번 줄의 Omit<Menu, 'id'>는 Menu에서 id 줄을 빼 item과 price 줄만 남깁니다. 7번 줄의 Partial<Menu>는 세 속성을 모두 선택 속성으로 바꿔 price만 있는 { price: 4800 }도 받습니다(검사 규칙 4 · 7).
다르게 써도 되는 곳: 6번 줄 전체를 Pick<Menu, 'item' | 'price'>로 써도 같은 모양이지만, 빈칸 자리(____<Menu, 'id'>)에는 'id'를 빼는 Omit이 들어갑니다.
틀리는 꼴:
- 6번 줄
Pick—id줄만 골라 내어, 9 · 10번 줄의item이 남는 속성(TS2353)으로 막히고 15번 줄의d.item도 TS2339로 막힙니다. - 7번 줄
Pick이나Omit— 두 번째 타입 칸(속성 이름)이 빠져 TS2314(requires 2 type argument(s)— 타입 인자가 둘 필요하다)로 막힙니다. any— 검사를 끈 답이라 받지 않습니다(any가 무난하다는 생각).
다시 볼 레슨: 있는 타입에서 새 타입 — Pick·Omit·Partial
문제 8
하고 싶은 일마다 쓸 기호나 낱말, 내장 타입 이름을 쓰세요.
- (가) 두 객체 타입
A와B의 속성 줄을 모두 합친 교차 타입을 적을 때A ____ B에 쓰는 기호 - (나) 객체 타입
Menu의 모든 속성 이름을 리터럴 유니언으로 꺼낼 때____ Menu에 쓰는 낱말 - (다) 배열이나 객체 리터럴 뒤에 붙여 넓히지 않고 리터럴과
readonly로 추론하게 하는 두 낱말(['tall'] ____의 빈칸) - (라) 객체 타입
Menu에서'item'속성 줄만 골라 새 타입을 만들 때____<Menu, 'item'>에 쓰는 내장 타입 이름
힌트
(가)는 extends와 같은 일을 하는 기호이고, (다)는 as로 시작하는 두 낱말입니다.
정답
받는 답: (가) & · (나) keyof · (다) as const · (라) Pick
틀리는 꼴:
- (가)
|— 둘 가운데 한쪽이면 되는 유니언이라, 두 타입이 함께 가진 속성만 확실히 쓸 수 있습니다. - (나)
typeof— 값에서 타입을 뽑는 낱말입니다. 있는 타입에서 키 이름을 꺼낼 때는keyof를 씁니다. - (나)
Object.keys— 실행 때 키 이름 배열을 만드는 자바스크립트 함수이고, 타입 자리에는 쓸 수 없습니다(타입과 값을 섞는 생각). - (다)
readonly— 타입 자리에 붙이는 낱말이고, 값 뒤에 붙여 추론을 좁힐 때는as const를 씁니다. - (라)
Omit— 지정한 속성을 뺀 나머지를 남기는 타입입니다.
다시 볼 레슨: 타입에 줄 더하기 — extends와 & · keyof와 속성 타입 꺼내기 · 값에서 타입 뽑기 — typeof와 as const · 있는 타입에서 새 타입 — Pick·Omit·Partial
문제 9
카페 앱이 사이즈별 추가 금액 extra와 메뉴별 판매 수 sold를 Record로 적었습니다. 14번 줄에서 n의 지금 타입은 무엇일까요? 검사 결과와 실행 결과도 각각 쓰세요. 검사에서 막히면 막히는 줄을 모두 씁니다.
type Size = 'tall' | 'grande';
const extra: Record<
Size,
number
> = {
tall: 0,
};
const sold: Record<
string,
number
> = {
라테: 4,
};
const n = sold['빙수'];
console.log(extra.tall, n + 1);
힌트
2번 줄의 Record<Size, number>와 8번 줄의 Record<string, number>에서 키 칸에 들어간 타입이 어떻게 다른지 보고, 14번 줄의 '빙수'가 실행 때 sold에 있는지 봅니다.
힌트 둘
키가 리터럴 유니언인 Record<Size, number>는 모든 키가 있어야 하고, 키가 string인 Record<string, number>는 인덱스 시그니처와 같아 없는 키를 꺼내도 검사기는 number로 봅니다.
정답
14번 줄에서 n의 지금 타입은 number이고, 검사는 2번 줄에서 막힙니다. 실행하면 0 NaN이 찍힙니다.
main.ts(2,7): error TS2741: Property 'grande' is missing in type '{ tall: number; }' but required in type 'Record<Size, number>'.0 NaN| 줄 | 이름 | 지금 타입 |
|---|---|---|
| 2 | extra | Record<Size, number> |
| 8 | sold | Record<string, number> |
| 14 | n | number |
- 2번 줄:
Record<Size, number>는tall과grande줄이 모두 있어야 하는 객체 타입인데,grande가 빠져 TS2741로 막힙니다(검사 규칙 4 · 7). - 14번 줄:
Record<string, number>는[k: string]: number와 같아, 어떤 글자 키로 꺼내도 검사기는n을number로 보고 15번 줄을 지나보냅니다. - 실행: Node는 타입을 지우고 돌려
extra.tall의0과, 없는 키'빙수'에서 나온undefined + 1인NaN을 찍습니다(검사 규칙 1).
흔한 오답 하나는 15번 줄의 실행 결과를 0 1이나 0 0으로 쓴 것입니다. n의 지금 타입이 number이니 값도 들어 있다고 본 것입니다(꺼낸 값은 늘 있다는 생각). 실행 결과를 비웠다면 2번 줄의 검사 오류 때문에 Node가 돌지 않는다고 본 것이고(검사 오류가 실행을 막는다는 생각), 2번 줄도 지나간다고 썼다면 Record<Size, number>를 Record<string, number>와 같게 본 것입니다.
JS 다시 보기: 없는 속성을 읽으면 undefined이고, undefined + 1은 NaN입니다(JS '객체: 이름 붙은 칸 묶음').
다시 볼 레슨: Record — 키 타입으로 만드는 객체 타입 · 키가 정해지지 않은 객체 — 인덱스 시그니처 · 타입은 실행 전에 지워진다
문제 10
독서 모임 앱이 책의 상태 state를 'wish'에서 'read'로 바꿉니다. 실행하면 read가 찍히지만 검사에서 막힙니다.
interface Book {
id: number;
title: string;
state: 'read' | 'wish';
}
function patchBook(
b: Book,
p: Partial<Book>,
): Book {
return { ...b, ...p };
}
위 선언에 이어 쓴 코드와 검사 결과입니다. 몇 번 줄의 무엇을 고쳐야 할까요? 고친 줄을 쓰세요.
const base: Book = {
id: 1,
title: '모모',
state: 'wish',
};
const edit = {
state: 'read',
};
const next = patchBook(
base,
edit,
);
console.log(next.state);
main.ts(22,3): error TS2345: Argument of type '{ state: string; }' is not assignable to parameter of type 'Partial<Book>'. Types of property 'state' are incompatible. Type 'string' is not assignable to type '"read" | "wish" | undefined'.힌트
들여 쓴 마지막 줄을 읽고, 22번 줄에서 넘기는 edit의 state 타입이 몇 번 줄에서 왜 string이 되었는지 거슬러 갑니다.
힌트 둘
타입을 적지 않고 만든 객체 리터럴의 속성은 string으로 넓어집니다. edit을 만드는 줄에서 타입을 적거나 넓힘을 막습니다.
정답
17번 줄을 const edit: Partial<Book> = {으로 고칩니다.
interface Book {
id: number;
title: string;
state: 'read' | 'wish';
}
function patchBook(
b: Book,
p: Partial<Book>,
): Book {
return { ...b, ...p };
}고친 코드를 위 타입 선언에 이어 씁니다.
const base: Book = {
id: 1,
title: '모모',
state: 'wish',
};
const edit: Partial<Book> = {
state: 'read',
};
const next = patchBook(
base,
edit,
);
console.log(next.state);read오류는 22번 줄에서 났지만 까닭은 17번 줄입니다. 타입 표기 없이 만든 객체 edit의 state는 'read'에서 string으로 넓어져(검사 규칙 3), Partial<Book>의 state 자리("read" | "wish" | undefined)에 들어가지 못합니다. 17번 줄에 : Partial<Book>을 적으면 'read'가 그 자리에서 리터럴로 검사됩니다.
- 다르게 고쳐도 되는 곳: 19번 줄을
} as const;로 고치거나 18번 줄을state: 'read' as const,로 고쳐도state가"read"로 유지되어 지나갑니다. 22번 줄에서 이름edit대신{ state: 'read' }를 바로 넘겨도 됩니다.
흔한 오답은 오류가 난 22번 줄을 // @ts-ignore나 edit as any로 덮거나, 4번 줄의 Book을 state: string;으로 넓히는 것입니다. 그렇게 덮으면 state: 'raed'처럼 오타를 내도 검사는 지나가고 실행 결과에 틀린 값 raed가 그대로 찍힙니다(오류 줄이 고칠 줄이라는 생각 · 넓어지지 않는다는 생각 · 검사를 지나면 맞다는 생각).
다시 볼 레슨: 있는 타입에서 새 타입 — Pick·Omit·Partial · 값에서 타입 뽑기 — typeof와 as const · 타입 오류 읽기
문제 11 (만들어 보기)
카페 주문 타입 Order 하나에서 새 주문 타입 NewOrder(id를 뺀 타입)와 수정 타입 OrderPatch(속성을 모두 선택으로 바꾼 타입)를 뽑아 고치기 함수 patchOrder를 만들어 봅니다. 다 채워 돌리면 이렇게 나와야 합니다.
라테 3
patchOrder의 둘째 인자를 { item: '녹차', qty: 1 }로 바꾸면 이렇게 나와야 합니다.
녹차 1
뼈대입니다. 주석 셋을 따라 8 · 9 · 12 · 13번 줄을 채우세요. 검사 오류는 0이어야 하고, 파생 타입을 손으로 다시 풀어 적거나 any로 덮은 답은 받지 않습니다.
// 1. 원본 타입 하나
interface Order {
id: number;
item: string;
qty: number;
}
// 2. Pick·Omit·Partial로 만들기
type NewOrder = ____;
type OrderPatch = ____;
// 3. 원본만 고치기
function patchOrder(
o: ____, p: ____,
): ____ {
return { ...o, ...p };
}
앞 코드에 이어 씁니다.
const draft: NewOrder = {
item: '라테',
qty: 2,
};
const saved: Order = {
id: 1,
...draft,
};
const r = patchOrder(
saved,
{ qty: 3 },
);
console.log(r.item, r.qty);
힌트
'2.'의 NewOrder는 Order에서 'id'를 빼고, OrderPatch는 Order의 모든 속성을 선택으로 바꿉니다. '3.'의 patchOrder는 원래 주문과 바꿀 부분만 받아 고친 주문을 돌려줍니다.
정답
모범 답입니다. 1~5번 줄이 '1. 원본 타입 하나', 6~9번 줄이 '2. Pick·Omit·Partial로 만들기', 10~15번 줄이 '3. 원본만 고치기'입니다.
interface Order {
id: number;
item: string;
qty: number;
}
type NewOrder =
Omit<Order, 'id'>;
type OrderPatch =
Partial<Order>;
function patchOrder(
o: Order,
p: OrderPatch,
): Order {
return { ...o, ...p };
}둘째 블록은 그대로 이어 씁니다.
const draft: NewOrder = {
item: '라테',
qty: 2,
};
const saved: Order = {
id: 1,
...draft,
};
const r = patchOrder(
saved,
{ qty: 3 },
);
console.log(r.item, r.qty);라테 3Omit<Order, 'id'>와 Partial<Order>로 만들어 두면, 나중에 Order에 속성이 바뀌어도 두 타입과 patchOrder가 함께 따라 바뀝니다(검사 규칙 4 · 7). 둘째 시험 값 { item: '녹차', qty: 1 }로 바꾸면 item과 qty가 덮어써져 녹차 1이 찍힙니다.
- 다르게 써도 되는 곳:
OrderPatch를Partial<NewOrder>로 써도(id는 바꾸지 않으므로) 이 코드는 지나갑니다.patchOrder의 반환 타입: Order를 생략해도{ ...o, ...p }에서 추론되어 같습니다. - 검사 오류가 0이고, 찍은 화면이 위와 같고, 둘째 시험 값도 맞으면 맞습니다.
- 흔한 오답 하나는
p: Order로 적은 것입니다. 그러면{ qty: 3 }처럼 일부 속성만 넘기는 26번 줄이 TS2739(빠진 속성들)로 막힙니다. Partial이면 검사가 꺼진다고 생각해{ qty: '3' }을 넘기면 TS2322로 막힙니다.Partial은 속성이 없어도 되게 할 뿐, 넣은 속성의 타입은 그대로 검사합니다.
다시 볼 레슨: 있는 타입에서 새 타입 — Pick·Omit·Partial
문제 12 (만들어 보기)
이번에는 빈 데서 씁니다. 도서관 열람실 구역 배열 zones에서 구역 타입 Zone을 뽑고, 구역마다 남은 자리 수를 담는 Seats(Record 사용)와 일부 구역만 고치는 SeatsPatch(Partial 사용), 그리고 자리표를 고쳐 돌려주는 함수 updateSeats를 만들어 봅니다. 다 채워 돌리면 이렇게 나와야 합니다.
A: 5, B: 8
patch를 { A: 2 }로 바꾸면 이렇게 나와야 합니다.
A: 2, B: 10
아래 시작 값에 이어서 타입 Zone · Seats · SeatsPatch, 함수 updateSeats, 그리고 base에 patch를 반영한 결과를 A: 5, B: 8 꼴로 찍는 코드를 쓰세요. 검사 오류는 0이어야 하고, any로 덮은 답은 받지 않습니다.
const zones = [
'A', 'B',
] as const;
const base = { A: 5, B: 10 };
const patch = { B: 8 };
힌트
하위 목표 셋(1. 원본 타입 하나 · 2. Pick·Omit·Partial로 만들기 · 3. 원본만 고치기)을 따릅니다. 먼저 zones에서 (typeof zones)[number]로 Zone을 뽑고, Record<Zone, number>로 원본 객체 타입 Seats를 만듭니다.
힌트 둘
SeatsPatch는 Partial<Seats>로 만들고, updateSeats는 b: Seats와 p: SeatsPatch를 받아 { ...b, ...p }를 돌려줍니다.
정답
모범 답입니다.
const zones = [
'A', 'B',
] as const;
type Zone =
(typeof zones)[number];
type Seats =
Record<Zone, number>;
type SeatsPatch =
Partial<Seats>;
function updateSeats(
b: Seats,
p: SeatsPatch,
): Seats {
return { ...b, ...p };
}앞 코드에 이어 씁니다.
const base: Seats = {
A: 5,
B: 10,
};
const patch: SeatsPatch = {
B: 8,
};
const r = updateSeats(
base,
patch,
);
console.log(
`A: ${r.A}, B: ${r.B}`,
);A: 5, B: 8zones에 as const를 붙여 Zone은 "A" | "B"가 되고, Seats는 A와 B가 모두 number인 객체 타입이 됩니다. SeatsPatch는 Partial<Seats>라 { B: 8 }처럼 한 구역만 있어도 됩니다. patch를 { A: 2 }로 바꾸면 A만 2로 바뀌고 B는 원래 값 10이 남아 A: 2, B: 10이 찍힙니다.
- 다르게 써도 되는 곳:
base와patch에 타입 표기를 생략해도 모양이 맞아updateSeats(base, patch)는 지나갑니다. 다만 선언 줄에: Seats와: SeatsPatch를 적어 두면 없는 구역'C'를 넣었을 때 만드는 줄에서 바로 잡힙니다(TS2353). - 검사 오류가 0이고, 찍은 화면이 위와 같고, 둘째 시험 값도 맞으면 맞습니다.
- 흔한 오답 하나는
zones에서as const를 빼먹은 것입니다. 그러면Zone이string으로 넓어져Seats가Record<string, number>가 되고,base에B가 빠져도 검사가 잡지 못합니다(넓어지지 않는다는 생각). - 다른 흔한 오답은
Seats만 쓰고updateSeats의 둘째 매개변수도p: Seats로 적은 것입니다. 그러면 한 구역만 고치는{ B: 8 }에서A가 빠졌다고 TS2741로 막힙니다.
다시 볼 레슨: 값에서 타입 뽑기 — typeof와 as const · Record — 키 타입으로 만드는 객체 타입 · 있는 타입에서 새 타입 — Pick·Omit·Partial