본문으로 건너뛰기

단원 연습 문제

타입스크립트 목차제네릭문제 12개

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

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

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

문제 1

성적 앱이 시험 점수 배열 grades를 두 함수에 넘깁니다. head는 목록의 첫 값을, wrap은 받은 값을 배열에 담아 돌려줍니다. 10번 줄에서 a와 b의 지금 타입은 각각 무엇일까요? 실행 결과도 쓰세요.

function head<T>(list: T[]) {
  return list[0];
}
function wrap<T>(v: T) {
  return [v];
}
const grades = [72, 88];
const a = head(grades);
const b = wrap(grades);
console.log(a, b);
힌트

8 · 9번 줄마다 인자 타입, T, 돌려주는 타입을 표에 적어 봅니다. 인자를 맞추는 칸은 1번 줄이 T[], 4번 줄이 T입니다.

힌트 둘

T 칸은 부르는 줄마다 새로 채웁니다. 인자 타입을 칸 모양에 맞춰, T[] 칸이면 배열의 원소 타입이, T 칸이면 인자 타입 통째가 T입니다.

정답

10번 줄에서 a의 지금 타입은 number, b는 number[][]입니다.

실행 결과
72 [ [ 72, 88 ] ]
줄인자 타입T돌려주는 타입
8number[]numbernumber
9number[]number[]number[][]

9번 줄은 새로 부르는 줄이라 T도 새로 채웁니다(검사 규칙 7). 같은 number[]라도 T 칸에는 통째로 맞춰져 T가 number[]이고, [v]가 한 겹 더 싸 b는 number[][]입니다.

흔한 오답 하나는 a를 number[]로 쓴 것입니다(인자 타입이 곧 T라는 생각). b를 number[]로, 찍히는 b를 [ 72, 88 ]로 썼다면 8번 줄의 T가 9번 줄에도 남는다고 본 것입니다(T가 한 번 정해진다는 생각).

JS 다시 보기: console.log는 배열 안의 배열을 대괄호 두 겹으로 찍습니다(JS '배열과 이름 둘').

다시 볼 레슨: 제네릭 — 부를 때 채우는 타입 칸

문제 2

카페 대기열에는 음료(Cup)와 원두(Bean) 주문이 섞여 있습니다. 맨 앞 주문을 꺼내 음료의 용량 ml을 찍으려 합니다.

interface Cup {
  kind: 'cup';
  ml: number;
}
interface Bean {
  kind: 'bean';
  g: number;
}
type Order = Cup | Bean;

위 타입 선언에 이어 씁니다. 검사를 지나갈까요? 지나가지 못하면 막히는 줄을 모두 쓰세요.

function peek<T>(q: T[]) {
  return q[0];
}
const queue: Order[] = [
  { kind: 'cup', ml: 355 },
  { kind: 'bean', g: 200 },
];
const o = peek(queue);
console.log(o.ml);
힌트

17번 줄에서 T 칸에 무엇이 채워지는지 표에 적고, 18번 줄의 o가 Cup인지 Bean인지 검사기가 아는지 봅니다.

힌트 둘

T는 넘긴 타입 그대로 채워집니다. 유니언에서는 모든 갈래에 있는 속성만 꺼낼 수 있고, 한 갈래로 좁히려면 kind를 확인합니다.

정답

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

검사 결과 오류 1개
main.ts(18,15): error TS2339: Property 'ml' does not exist on type 'Order'.  Property 'ml' does not exist on type 'Bean'.
줄인자 타입T돌려주는 타입
17Order[]OrderOrder

TS2339의 Property 'ml' does not exist⁠는 'ml 속성이 없다'는 뜻이고, 둘째 줄이 Bean 갈래에 없다고 짚습니다. 17번 줄은 T를 Order로 채워, o는 두 갈래가 다 남은 Order입니다. 검사기는 14번 줄의 첫 주문이 음료라는 값을 따라가지 않습니다(검사 규칙 5). 18번 줄을 if (o.kind === 'cup') 갈래 안에 넣으면 o가 Cup으로 좁혀져 지나고, 355가 찍힙니다.

흔한 오답은 '지나감'입니다. 첫 주문이 음료라 T가 Cup으로 채워지고 o에 ml이 있다고 본 것입니다(검사기가 실행을 따라간다는 생각).

다시 볼 레슨: 판별 유니언 · 제네릭 — 부를 때 채우는 타입 칸

문제 3

독서 앱이 책마다 별점 stars를 매깁니다. fav는 별이 넷 이상인지 보고, up은 책 제목을 찍은 뒤 별을 하나 올려 그 책을 돌려줍니다.

interface Rated {
  stars: number;
}
function fav<T>(x: T) {
  return x.stars >= 4;
}
function up<T extends Rated>(
  x: T,
) {
  console.log(x.title);
  x.stars += 1;
  return x;
}

위 선언에 이어 씁니다. 검사 결과와 실행 결과를 각각 쓰세요. 검사에서 막히면 막히는 줄을 모두 씁니다.

const m = {
  title: '모모',
  stars: 4,
};
console.log(fav(m));
console.log(up(m).title);
힌트

5 · 10 · 11 · 19번 줄에서 점 뒤의 속성(stars · title)을 점 앞의 타입이 아는지 봅니다. 4번 줄과 7번 줄의 T가 어떻게 다른지도 봅니다.

힌트 둘

조건 없는 T는 무엇이든 될 수 있어 속성을 모르고, extends Rated를 단 T는 Rated의 속성만 압니다. 돌려받는 T는 넘긴 타입 그대로 채워집니다. 검사 오류가 있어도 Node는 타입을 지우고 돌립니다.

정답

검사는 5번 줄과 10번 줄에서 막히고, 실행하면 true와 모모 두 번이 찍힙니다.

검사 결과 오류 2개
main.ts(5,12): error TS2339: Property 'stars' does not exist on type 'T'.main.ts(10,17): error TS2339: Property 'title' does not exist on type 'T'.
실행 결과
true
모모
모모
견줄 곳4번 줄 <T>7번 줄 <T extends Rated>
T 칸에 오는 타입무엇이든Rated에 드는 타입
함수 안에서 아는 속성없음stars
18 · 19번 줄의 Tm의 타입 그대로m의 타입 그대로

조건 없는 T는 5번 줄에서 stars를 모르고, extends Rated의 T는 stars만 알아 10번 줄의 title에서 막힙니다(TS2339 — 문제 2와 같은 오류). 19번 줄은 돌려받은 값에 title이 남아 지나갑니다. 검사 오류가 있어도 Node는 타입을 지우고 돌립니다(검사 규칙 1).

흔한 오답 하나는 넘긴 m에 stars와 title이 있으니 5번 줄이나 10번 줄이 지나간다고 본 것입니다(T는 아무거나라는 생각). 19번 줄도 막힌다고 썼다면 up이 Rated를 돌려준다고 본 것이고(조건 타입으로 줄어든다는 생각), 실행 결과를 비웠다면 검사 오류 때문에 Node가 돌지 않는다고 본 것입니다(검사 오류가 실행을 막는다는 생각).

다시 볼 레슨: 제약 — extends로 칸에 조건 달기 · 타입은 실행 전에 지워진다

문제 4

카페 진열대 앱이 케이크 판과 컵 판을 한 타입 Tray로 나타냅니다. 판마다 이름 label과 놓인 것들 items가 있고, 케이크 판에는 글자가, 컵 판에는 수가 놓입니다. 이렇게 나와야 합니다.

2 480

검사를 지나고 실행 결과가 위처럼 되도록 1번 줄과 3번 줄의 빈칸을 채우세요. any로 덮은 답은 받지 않습니다.

interface ____ {
  label: string;
  items: ____;
}
const cakes: Tray<string> = {
  label: '케이크',
  items: ['치즈', '초코'],
};
const cups: Tray<number> = {
  label: '컵',
  items: [240, 355],
};
const a = cakes.items[0];
const b = cups.items[0];
console.log(a.length, b * 2);
힌트

5 · 9번 줄의 Tray<string> · Tray<number>에 적은 타입이 1번 줄 어디에 들어가는지, 15번 줄이 지나려면 a · b가 어떤 타입이어야 하는지 봅니다.

힌트 둘

타입 이름 뒤에 <T>를 붙이면 칸이 열리고, 칸에 채운 타입이 T를 쓴 자리마다 들어갑니다. 배열 타입은 string[]처럼 원소 타입 뒤에 []를 붙입니다.

정답

받는 답: 1번 줄 Tray<T> · 3번 줄 T[]

1번 줄의 <T>가 Tray에 타입 칸 하나를 엽니다. 5번 줄은 그 칸에 string을, 9번 줄은 number를 채워 3번 줄의 items가 판마다 string[]과 number[]가 됩니다(검사 규칙 7).

같은 답으로 치는 꼴: 3번 줄을 Array<T>로 쓴 것(T[]와 같은 타입), 칸 이름을 Tray<V>와 V[]처럼 다른 이름으로 맞춰 쓴 것.

다르게 써도 되는 곳: 3번 줄을 readonly T[]나 튜플 [T, T]로 써도 이 코드는 지나갑니다. 칸을 열어 T를 쓴 것은 같습니다.

틀리는 꼴:

  • 1번 줄 Tray — 칸을 열지 않아 3번 줄의 T를 찾지 못하고(TS2304), 5 · 9번 줄도 TS2315(Type 'Tray' is not generic)⁠로 막힙니다.
  • 3번 줄 T — items가 글자 하나 · 수 하나가 되어, 7 · 11번 줄의 배열이 들어가지 못합니다(TS2322).
  • 3번 줄 string[] — 칸을 쓰지 않아 11번 줄의 수 둘이 들어가지 못하고(TS2322), b가 글자라 15번 줄의 b * 2도 TS2362로 막힙니다.
  • 3번 줄 (string | number)[] — a · b가 string | number라 15번 줄의 a.length(TS2339)와 b * 2(TS2362)가 막힙니다.
  • 3번 줄 any — 2 480은 찍히지만 검사를 끈 답이라 받지 않습니다(any가 무난하다는 생각).

다시 볼 레슨: 제네릭 타입 — Array·Map·내 타입

문제 5

카페 적립 앱이 손님마다 방문 수를 Map에 적어 둡니다. 오늘 온 손님의 방문 수에 1을 더해 찍는데, 처음 온 태오는 적힌 값이 없으니 1을 찍어야 합니다. 그런데 검사에서 막히고, 돌리면 NaN이 찍힙니다.

const visit =
  new Map<string, number>();
visit.set('유나', 9);
const c = visit.get('태오');
console.log(c + 1);
검사 결과 오류 1개
main.ts(5,13): error TS18048: 'c' is possibly 'undefined'.
실행 결과
NaN

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

  • ① 5번 줄을 console.log(c! + 1);로 고친다
  • ② 5번 줄을 console.log((c ?? 0) + 1);⁠로 고친다
  • ③ 5번 줄 앞에 // @ts-ignore 한 줄을 더한다
  • ④ 5번 줄을 if (c instanceof Number) {⁠와 } 사이에 넣는다
힌트

4번 줄의 c에 어떤 값이 붙는지 보고, 고치기마다 5번 줄의 c를 검사기가 보는 타입과 실행 때의 값으로 나눠 따라가 봅니다.

힌트 둘

Map의 get은 없는 키에 undefined를 돌려줘, 돌려주는 타입이 number | undefined입니다. !와 // @ts-ignore는 검사기의 말만 막고 값은 그대로 둡니다.

정답

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

TS18048의 'c' is possibly 'undefined'⁠는 'c는 undefined일 수 있다'는 뜻입니다. get은 없는 키면 undefined를 돌려줘 c가 number | undefined이고, 넣지 않은 태오의 c는 undefined입니다. ②의 ??가 0을 대신 써 1을 찍습니다.

  • ①과 ③은 검사기의 말만 막고 값은 그대로라 NaN입니다.
  • ④는 TS2358로 막힙니다. instanceof의 왼쪽에는 객체가 와야 하는데(an object type), c는 수나 undefined입니다. 값이 있어도 수는 Number 객체가 아니라 갈래에 들지 않습니다.

흔한 오답 하나는 ①이나 ③도 1을 찍는다고 고른 것입니다(검사를 지나면 맞다는 생각). ④를 골랐다면 instanceof로 수인지 확인된다고 본 것입니다(instanceof로 타입을 확인하는 생각).

JS 다시 보기: Map의 get은 없는 키에 undefined를 돌려주고, undefined + 1은 NaN입니다(JS 'Map과 Set').

다시 볼 레슨: 제네릭 타입 — Array·Map·내 타입 · null·undefined 거르기

문제 6

시간표 앱이 요일마다 과목을 Map에 적어 둡니다. 화요일 과목의 글자 수를 찍으려 합니다. 검사 결과와 실행 결과를 각각 쓰세요.

const week = new Map();
week.set('월', '수학');
const sub = week.get('화');
console.log(sub.length);
힌트

1번 줄의 new Map()에 칸을 적지 않은 것과, 3번 줄의 sub에 실행 때 어떤 값이 붙는지 봅니다.

힌트 둘

칸을 적지 않은 new Map()은 두 칸이 any로 채워져, get이 돌려주는 값도 any입니다. 2번 줄의 set은 칸을 바꾸지 않습니다.

정답

검사는 오류 없이 지나가고, 실행하면 4번 줄에서 TypeError로 멈춥니다.

검사 결과 오류 없음
실행 결과
main.ts:4console.log(sub.length);                ^ TypeError: Cannot read properties of undefined (reading 'length')    at main.ts:4:17
줄이름지금 타입
2weekMap<any, any>
4subany

칸을 적지 않은 Map은 두 칸이 any라, get이 돌려준 sub도 any이고 검사가 꺼집니다(검사 규칙 6). 실행 때 '화'는 넣지 않은 키라 sub가 undefined이고, 그 length를 읽다 멈춥니다. 1번 줄을 new Map<string, string>()⁠으로 적으면 검사가 4번 줄을 TS18048로 미리 막습니다.

흔한 오답 하나는 2번 줄의 set으로 칸이 글자로 채워져 4번 줄이 막힌다고 본 것입니다(검사기가 실행을 따라간다는 생각). 실행도 멈추지 않는다고 썼다면 any면 무엇이든 돌아간다고 본 것입니다(any가 무난하다는 생각).

JS 다시 보기: undefined의 속성을 읽으면 TypeError로 멈춥니다(JS '객체: 이름 붙은 칸 묶음').

다시 볼 레슨: any는 검사를 끈다 · 제네릭 타입 — Array·Map·내 타입

문제 7

사물함 앱이 칸 번호 box와 비밀번호 pin을 타이머로 조금 뒤에 찍습니다. 그 사이 비밀번호는 지웁니다. 4번 줄의 box와 5번 줄의 pin의 지금 타입은 각각 무엇일까요? 검사 결과와 실행 결과도 각각 쓰세요.

let box: string | null = 'A12';
let pin: string | null = '0412';
setTimeout(() => {
  console.log(box.length);
  console.log(pin.length);
}, 0);
pin = null;
힌트

1 · 2 · 4 · 5 · 7번 줄의 box와 pin을 타입 추적 표에 적어 봅니다.

힌트 둘

나중에 불릴 콜백 안에서는, 콜백을 만든 뒤에 다시 대입되는 이름의 좁힘이 남지 않습니다. 다시 대입되지 않는 이름은 좁힘이 남습니다. 콜백은 7번 줄까지 다 돈 뒤에 불립니다.

정답

4번 줄에서 box의 지금 타입은 string, 5번 줄의 pin은 string | null이고, 검사는 5번 줄에서 막힙니다. 실행하면 3을 찍은 뒤 5번 줄에서 TypeError로 멈춥니다.

검사 결과 오류 1개
main.ts(5,15): error TS18047: 'pin' is possibly 'null'.
실행 결과
3main.ts:5  console.log(pin.length);                  ^ TypeError: Cannot read properties of null (reading 'length')    at Timeout._onTimeout (main.ts:5:19)
줄이름지금 타입
1boxstring
2pinstring
4boxstring — 다시 대입되는 곳이 없음
5pinstring | null — 7번 줄에서 다시 대입됨
7pinnull

TS18047의 'pin' is possibly 'null'⁠은 'pin은 null일 수 있다'는 뜻입니다. 1번 줄에서 글자로 좁혀진 box는 다시 대입되는 곳이 없어 콜백 안에서도 string입니다. pin은 콜백을 만든 뒤 7번 줄에서 다시 대입되니, 콜백이 언제 불릴지 모르는 검사기는 5번 줄의 pin을 선언 타입 string | null로 봅니다(검사 규칙 5). 실행 때 콜백은 7번 줄까지 다 돈 뒤에 불려, pin이 이미 null입니다. 검사기 말이 맞았습니다.

콜백 밖에서 const p = pin;으로 받아 p를 찍으면 검사 오류가 0이고 3과 4가 찍힙니다.

같은 답으로 치는 꼴: pin을 null | string처럼 차례만 바꿔 쓴 것.

흔한 오답 하나는 pin을 string으로 쓰고 '지나감'이라고 쓴 것입니다(좁힘이 끝까지 간다는 생각). box도 string | null로 썼다면 콜백 안에서는 좁힘이 모두 풀린다고 본 것입니다(갈래를 나오면 선언 타입이라는 생각). 실행 결과를 3과 4로 썼다면 콜백이 7번 줄보다 먼저 돈다고 본 것입니다.

JS 다시 보기: setTimeout의 콜백은 0을 줘도 지금 코드가 다 끝난 뒤에 불리고, 그때 바깥 이름의 값을 읽습니다(JS '나중에 하기: setTimeout' · JS '클로저: 만들어진 칸을 기억하는 함수').

다시 볼 레슨: Promise와 async의 타입 · 좁힘이 달라지는 곳 — 대입과 반복

문제 8

카페 번호표 화면이 주문을 불러온 결과를 받습니다. 불러왔으면(got) 받은 번호 data에 1을 더한 다음 번호를, 못 불러왔으면(miss) 까닭 why를 돌려줍니다.

interface Got<T> {
  kind: 'got';
  data: T;
}
interface Miss {
  kind: 'miss';
  why: string;
}
type Load<T> = Got<T> | Miss;

위 타입 선언에 이어 씁니다. 16번 줄의 r과 22번 줄의 v의 지금 타입은 각각 무엇일까요? 실행 결과도 쓰세요.

async function show(
  r: Load<number>,
) {
  if (r.kind === 'miss') {
    return r.why;
  }
  return r.data + 1;
}
const v = show({
  kind: 'got',
  data: 4,
});
console.log(await v);
힌트

13 · 14 · 16번 줄의 r을 타입 추적 표에 적어 봅니다. show가 돌려주는 값은 return 줄에서 봅니다.

힌트 둘

async 함수는 return한 값의 타입을 Promise 상자의 T 칸에 담아 돌려주고, return이 둘이면 두 타입의 유니언이 칸에 들어갑니다. await는 상자를 열어 값을 꺼냅니다.

정답

16번 줄에서 r의 지금 타입은 Got<number>, 22번 줄의 v는 Promise<string | number>⁠입니다.

실행 결과
5
줄이름지금 타입
13rLoad<number> → 참 Miss 거짓 Got<number>
14rMiss
16rGot<number>
22vPromise<string | number>

13번 줄의 참 갈래는 return으로 끝나 16번 줄의 r은 Got<number>입니다. show는 14번 줄에서 글자를, 16번 줄에서 수를 돌려줄 수 있어, async가 둘을 상자에 담은 Promise<string | number>⁠를 돌려줍니다(검사 규칙 3 · 7). 실행 때는 22번 줄의 await가 상자를 열어 5가 찍힙니다.

22번 줄에서 await를 빼면 상자째 Promise { 5 }가 찍힙니다.

같은 답으로 치는 꼴: v를 Promise<number | string>⁠처럼 칸 안 차례만 바꿔 쓴 것.

흔한 오답 하나는 v를 number나 string | number로 쓴 것입니다(async 함수가 값을 돌려준다는 생각). Promise<number>로 썼다면 18번 줄에 got을 넘겼으니 16번 줄의 return만 친 것입니다(검사기가 실행을 따라간다는 생각). 16번 줄의 r을 Load<number>로 썼다면 return으로 끝난 갈래를 놓친 것입니다(갈래를 나오면 선언 타입이라는 생각).

JS 다시 보기: async 함수는 늘 Promise를 돌려주고, await가 그 값을 꺼냅니다(JS 'async와 await').

다시 볼 레슨: Promise와 async의 타입 · 판별 유니언

문제 9

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

  • (가) 함수의 타입 칸 T에 'Priced에 드는 타입만'이라는 조건을 달 때, <T ____ Priced>의 빈칸에 쓰는 낱말
  • (나) 수를 return하는 async 함수의 돌려주는 타입
  • (다) m.set('A', 3)과 m.get('A')처럼 글자 키에 수 값을 넣고 꺼내는 m의 타입(칸까지 채워서)
  • (라) async 함수 안에서 Promise 상자를 열어 안의 값을 꺼내는 낱말
힌트

(가)는 low에서, (나)와 (라)는 getPrice에서 본 꼴입니다. (다)는 칸이 둘인 내장 타입입니다.

정답

받는 답: (가) extends · (나) Promise<number> · (다) Map<string, number> · (라) await

같은 답으로 치는 꼴: (다)를 쉼표 뒤 띄어쓰기 없이 Map<string,number>로 쓴 것.

틀리는 꼴:

  • (가) Extends — 작은 글자로만 씁니다(TS1005).
  • (나) number — async 함수의 돌려주는 타입은 Promise여야 해 TS1064로 막힙니다(Did you mean to write 'Promise<number>'?).
  • (나) Promise나 (다) Map — 칸을 채우지 않아 TS2314로 막힙니다.
  • (다) Map<number, string> — 키 칸과 값 칸이 바뀌어, 이 타입의 m에서는 m.set('A', 3)이 TS2345로 막힙니다.
  • (라) async — 함수가 Promise를 돌려주게 하는 낱말이고, 상자를 여는 것은 await입니다.

다시 볼 레슨: 제약 — extends로 칸에 조건 달기 · 제네릭 타입 — Array·Map·내 타입 · Promise와 async의 타입

문제 10

음악 앱이 재생 목록에 든 곡 길이 sec를 모두 더해 찍습니다. 아래 두 곡이면 380이 찍혀야 하는데, 검사에서 막힙니다.

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

interface Song {
  sec: number;
}
function sum<T extends Song>(
  list: T[],
) {
  let s = 0;
  for (const x of list) {
    s += x.sec;
  }
  return s;
}
const a = { sec: 180 };
const b = { secs: 200 };
console.log(sum([a, b]));
검사 결과 오류 1개
main.ts(15,21): error TS2741: Property 'sec' is missing in type '{ secs: number; }' but required in type 'Song'.
힌트

15번 줄에서 넘기는 a와 b의 속성 이름을 4번 줄의 조건 Song과 하나씩 맞춰 봅니다.

힌트 둘

조건을 단 T 칸에는 조건의 속성을 모두 가진 타입만 들어갑니다. 오류는 넘기는 줄에서 짚히지만, 모양이 틀린 값은 그 위에서 만들었습니다.

정답

14번 줄을 const b = { sec: 200 };⁠으로 고칩니다. 고쳐 돌리면 180과 200을 더한 380이 찍힙니다.

TS2741의 Property 'sec' is missing⁠과 but required in type 'Song'⁠은 'sec 속성이 빠졌는데, Song에는 꼭 있어야 한다'는 뜻입니다. 오류는 15번 줄에서 났지만 까닭은 14번 줄입니다. b의 속성 이름이 secs로 잘못 적혀 4번 줄의 조건 Song에 들지 않습니다.

  • 다르게 고쳐도 되는 곳: 14번 줄을 const b: Song = { sec: 200 };⁠처럼 타입까지 적어도 됩니다. 그러면 secs로 잘못 적었을 때 14번 줄이 바로 막힙니다(TS2561).

흔한 오답은 오류가 난 15번 줄만 덮는 것입니다. 15번 줄 위에 // @ts-ignore를 더하거나, 14번 줄을 const b: any =로 받거나 { secs: 200 } as any로 덮으면 검사는 지납니다. 그래도 b에 sec가 없어 180 + undefined가 되어 NaN이 찍힙니다. 덮는 길은 오류만 지우고 값과 실행 결과는 그대로 둡니다(오류 줄이 고칠 줄이라는 생각 · 검사를 지나면 맞다는 생각).

JS 다시 보기: 없는 속성을 읽으면 undefined이고, 수에 undefined를 더하면 NaN입니다(JS '객체: 이름 붙은 칸 묶음').

다시 볼 레슨: 제약 — extends로 칸에 조건 달기 · 타입 오류 읽기

문제 11 (만들어 보기)

가게 목록에서 남은 개수 qty가 0보다 큰 것만 골라 내는 함수 keep을 만들어 봅니다. 케이크 목록에도 원두 목록에도 같은 함수를 쓰고, 골라 낸 값에서 이름(name · bean)도 꺼낼 수 있어야 합니다. 다 채워 돌리면 이렇게 나와야 합니다.

당근 케냐

뼈대입니다. 주석 셋을 따라 6~8번 줄에 타입을 채우세요. 검사 오류는 0이어야 하고, any로 덮은 답은 받지 않습니다.

interface Qty {
  qty: number;
}
// 1. 바뀌는 타입을 T로
// 2. 쓰는 속성은 extends로
function keep(
  list,
) {
  // 3. 돌려주는 값은 T로
  return list.filter(
    (x) => x.qty > 0,
  );
}

앞 코드에 이어 씁니다.

const cakes = [
  { name: '레몬', qty: 0 },
  { name: '당근', qty: 2 },
];
const beans = [
  { bean: '케냐', qty: 5 },
];
const a = keep(cakes)[0];
const b = keep(beans)[0];
console.log(a.name, b.bean);

다 쓰면 15번 줄의 레몬 qty를 1로 바꿔 따라가 봅니다. 레몬 케냐여야 합니다.

힌트

'1.'은 6번 줄의 함수 이름 뒤에 칸을 열고, 7번 줄의 list를 그 칸의 배열로 적습니다. '2.'는 11번 줄에서 꺼내는 qty를 칸의 조건으로 답니다. '3.'은 23번 줄에서 name과 bean을 꺼내야 하니, 8번 줄의 돌려주는 타입을 칸으로 적습니다.

정답

모범 답입니다. 첫째 블록의 6~8번 줄을 채웁니다.

interface Qty {
  qty: number;
}
// 1. 바뀌는 타입을 T로
// 2. 쓰는 속성은 extends로
function keep<T extends Qty>(
  list: T[],
): T[] {
  // 3. 돌려주는 값은 T로
  return list.filter(
    (x) => x.qty > 0,
  );
}

둘째 블록은 그대로 이어 씁니다.

const cakes = [
  { name: '레몬', qty: 0 },
  { name: '당근', qty: 2 },
];
const beans = [
  { bean: '케냐', qty: 5 },
];
const a = keep(cakes)[0];
const b = keep(beans)[0];
console.log(a.name, b.bean);
실행 결과
당근 케냐

6번 줄의 <T extends Qty>가 칸 T를 열고(1.) qty가 있는 타입만 받게 해(2.), 11번 줄의 x.qty를 쓸 수 있습니다. 21번 줄은 T를 cakes의 원소 타입으로, 22번 줄은 beans의 원소 타입으로 새로 채웁니다. 8번 줄의 T[]로 돌려받은 값에 name과 bean이 남아(3.), 23번 줄에서 꺼낼 수 있습니다. 15번 줄의 레몬 qty를 1로 바꾸면 레몬도 골라져 첫 값이 레몬이니 레몬 케냐입니다.

  • 다르게 써도 되는 곳: 8번 줄을 ) { 그대로 두어도 filter가 T[]를 돌려줘 같습니다. 칸 이름은 T가 아니어도 됩니다.
  • 검사 오류가 0이고, 찍은 화면이 위와 같고, 레몬을 바꾼 레몬 케냐도 맞으면 맞습니다.
  • 흔한 오답 하나는 칸 없이 7번 줄을 list: Qty[]로 쓴 것입니다. 돌려받은 값이 Qty라 name과 bean이 사라져, 23번 줄에서 TS2339로 막힙니다(조건 타입으로 줄어든다는 생각). 칸은 열고 8번 줄만 ): Qty[] {로 적어도 같습니다.
  • <T>만 열고 extends를 빼면 11번 줄의 x.qty에서 TS2339로 막힙니다(T는 아무거나라는 생각).
  • list를 any[]로 적으면 검사가 꺼져, 23번 줄의 bean을 baen으로 잘못 써도 막히지 않고 당근 undefined가 찍힙니다(any가 무난하다는 생각).

다시 볼 레슨: 제약 — extends로 칸에 조건 달기 · 제네릭 — 부를 때 채우는 타입 칸

문제 12 (만들어 보기)

이번에는 빈 데서 씁니다. 목록을 받아 id로 찾는 Map으로 바꿔 돌려주는 함수 toMap을 만들어 봅니다. 원소는 글자 id만 있으면 무엇이든 되고, 돌려받은 Map에서 꺼낸 값에 원소의 다른 속성도 남아야 합니다. 조건 타입 Id, 함수 toMap, 두 목록을 바꾸고 꺼내 찍는 코드를 쓰세요.

메뉴 목록은 차례로 { id: 'A1', item: '자몽' } · { id: 'B2', item: '보리' }⁠이고, 직원 목록은 { id: 'S1', who: '하은' } 하나입니다. 메뉴에서 'B2'의 item을, 직원에서 'S1'의 who를 꺼내 한 줄에 찍습니다. 이렇게 나와야 합니다. 검사 오류는 0이어야 하고, any로 덮은 답은 받지 않습니다.

보리 하은

다 쓰면 직원에서 꺼내는 id를 'S2'로 바꿔 따라가 봅니다. 보리 undefined가 찍히고 멈추지 않아야 합니다.

힌트

하위 목표 셋(1. 바뀌는 타입을 T로 · 2. 쓰는 속성은 extends로 · 3. 돌려주는 값은 T로)을 따릅니다. 문제 11과 같은 틀이고, 여기서 돌려주는 값은 키가 글자, 값이 T인 Map입니다.

힌트 둘

Map을 만들 때 칸을 적지 않으면 두 칸이 any라 검사가 꺼집니다. get은 없는 키에 undefined를 줄 수 있습니다.

정답

모범 답입니다.

interface Id {
  id: string;
}
function toMap<T extends Id>(
  list: T[],
) {
  const m =
    new Map<string, T>();
  for (const x of list) {
    m.set(x.id, x);
  }
  return m;
}

앞 코드에 이어 씁니다.

const menu = toMap([
  { id: 'A1', item: '자몽' },
  { id: 'B2', item: '보리' },
]);
const staff = toMap([
  { id: 'S1', who: '하은' },
]);
const a = menu.get('B2');
const b = staff.get('S1');
console.log(a?.item, b?.who);
실행 결과
보리 하은
줄이름지금 타입
21menuMap<string, { id: string; item: string; }>
23a{ id: string; item: string; } | undefined

4번 줄의 <T extends Id>가 '1.'과 '2.'입니다. 칸 T를 열고, 10번 줄에서 x.id를 쓰니 Id 조건을 답니다. 8번 줄의 new Map<string, T>()가 '3.'입니다. 값 칸을 T로 채워, 돌려받은 Map에서 꺼낸 값에 item과 who가 남습니다. get은 없는 키에 undefined를 줄 수 있어 23번 줄의 a가 … | undefined이니 ?.로 읽습니다. 직원 id를 'S2'로 바꾸면 b가 undefined라 b?.who도 undefined이고, 보리 undefined가 찍힙니다.

  • 다르게 써도 되는 곳: 6번 줄을 ): Map<string, T> {로 적어 돌려주는 타입을 밝혀도 됩니다. 칸 이름은 T가 아니어도 됩니다.
  • 검사 오류가 0이고, 찍은 화면이 위와 같고, 'S2'로 바꾼 보리 undefined도 맞고, 꺼낸 메뉴 값의 item을 itme으로 바꿨을 때 검사가 막으면 맞습니다. itme이 지나간다면 값 칸이 any라 검사가 꺼진 것입니다.
  • 흔한 오답 하나는 8번 줄을 칸 없이 new Map()으로 쓴 것입니다. m이 Map<any, any>라 검사가 꺼져, 23번 줄의 item을 itme으로 잘못 써도 막히지 않고 undefined 하은이 찍힙니다(any가 무난하다는 생각).
  • 값 칸을 Id로 채워 Map<string, Id>로 쓰면 꺼낸 값이 Id라 item과 who가 사라져, 23번 줄에서 TS2339로 막힙니다(조건 타입으로 줄어든다는 생각).
  • 23번 줄에서 ?.를 빼면 TS18048로 막힙니다. 그대로 돌리면 'S1'에서는 보리 하은이 찍히지만, 'S2'로 바꾸면 undefined의 who를 읽다 TypeError로 멈춥니다(꺼낸 값은 늘 있다는 생각).
  • 4번 줄에서 extends를 빼면 10번 줄의 x.id에서 TS2339로 막힙니다(T는 아무거나라는 생각).

다시 볼 레슨: 제네릭 타입 — Array·Map·내 타입 · 제약 — extends로 칸에 조건 달기

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

타입스크립트 목차로