제약(constraint)은 <T extends Priced>처럼 타입 칸 T에 조건을 달아, 그 조건을 지나는 타입으로만 T를 채우게 하는 문법입니다. 조건을 달면 함수 안에서 T 값의 속성을 쓸 수 있습니다.
제네릭 — 부를 때 채우는 타입 칸에서 T 칸은 부를 때 넘긴 인자로 채웠습니다. 무엇으로든 채워질 수 있으니, 함수를 만들 때는 T 값에 어떤 속성이 있는지 모릅니다.
이 레슨에서는 목록에서 가장 싼 것을 돌려주는 함수 low에 조건을 달고, 조건을 지나는 인자와 지나지 못하는 인자를 견줍니다.
JS 다시 보기: 가장 큰 값은 첫 값을 best에 두고, 값을 하나씩 꺼내 best보다 크면 best를 그 값으로 바꿔 가며 찾습니다. 가장 작은 값은 견주는 쪽만 뒤집습니다(JS '가장 큰 값 찾기').
1조건 없는 T는 속성을 모른다
메뉴 목록에서 가장 싼 메뉴를 돌려주는 함수를 제네릭으로 써 봅니다. 메뉴는 이름과 값을 가진 객체라, 값을 견주려면 원소의 price를 꺼내야 합니다.
function low<T>(items: T[]) {
let best = items[0];
for (const x of items) {
if (x.price < best.price) {
best = x;
}
}
return best;
}
main.ts(4,11): error TS2339: Property 'price' does not exist on type 'T'.main.ts(4,24): error TS2339: Property 'price' does not exist on type 'T'.4번 줄이 두 번 막힙니다. 열 11은 x.price, 열 24는 best.price의 price이고, 오류 문장은 'T 타입에 price 속성이 없다'는 뜻입니다.
items가 T[]라 꺼낸 원소 x도, 첫 원소를 붙인 best도 T입니다. T 칸은 부를 때 수로도 글자로도 채워질 수 있습니다. 그래서 함수를 만들 때 검사기는 T 값에 price가 있다고 보지 않습니다.
price를 쓰려면 'T 칸에는 price가 있는 타입만 온다'는 조건이 있어야 합니다.
2extends로 T 칸에 조건 달기
조건은 타입 매개변수 T 뒤에 extends와 조건 타입을 적어 답니다. 조건 타입 Priced는 interface로 먼저 적어 둡니다.
interface Priced {
price: number;
}
function low<T extends Priced>(
items: T[],
) {
let best = items[0];
for (const x of items) {
if (x.price < best.price) {
best = x;
}
}
return best;
}
4~6번 줄은 폰 화면에 맞추려고 매개변수에서 줄을 나눈 함수 머리입니다. <T extends Priced>는 'T 칸에는 Priced에 드는 타입만 온다'고 읽습니다.
이제 9번 줄은 막히지 않습니다. T가 무엇으로 채워지든 Priced에 드는 타입이라 price가 있기 때문입니다.
앞 코드에 이어 메뉴 목록을 넘겨 봅니다.
interface Menu {
name: string;
price: number;
}
const menu: Menu[] = [
{ name: '커피', price: 4000 },
{ name: '라테', price: 4500 },
{ name: '녹차', price: 3500 },
];
const cheap = low(menu);
console.log(cheap.name);
녹차24번 줄에서 검사기는 인자 menu의 타입 Menu[]를 매개변수 T[]에 맞춰 T 칸에 Menu를 넣어 보고, 그 Menu가 조건 Priced에 드는지 봅니다.
Menu는 Priced라는 이름을 적은 적이 없지만 price: number 줄이 있으니 듭니다. 검사기는 이름이 아니라 모양으로 맞추고, name처럼 줄이 더 있어도 듭니다(모양으로 맞추기 · 검사 규칙 4). 그래서 여기 extends는 클래스 상속이 아니라 'Priced에 드는'이라는 뜻입니다.
조건을 지나면 T 칸은 Menu로 채워집니다. 돌려주는 타입이 T라 cheap은 Menu이고, 25번 줄에서 cheap.name을 쓸 수 있습니다. 실행하면 가장 싼 녹차가 찍힙니다.
| 견줄 곳 | 조건 없는 <T>(1절) | <T extends Priced> |
|---|---|---|
| T 칸에 오는 타입 | 무엇이든 | Priced에 드는 타입 |
함수 안 x.price | TS2339로 막힘 | 쓸 수 있음 |
low(menu)의 T | Menu | Menu |
조건은 T 칸에 올 타입을 거를 뿐, 지난 타입을 Priced로 줄이지 않습니다. 넘긴 타입이 그대로 T 칸을 채웁니다(검사 규칙 7).
3조건을 지나지 못하는 인자
price가 없는 목록을 넘기면 어떻게 될까요? 1~14번 줄은 2절의 low 그대로입니다.
interface Priced {
price: number;
}
function low<T extends Priced>(
items: T[],
) {
let best = items[0];
for (const x of items) {
if (x.price < best.price) {
best = x;
}
}
return best;
}
이어서 이름만 적은 메뉴판을 넘깁니다. 19번 줄은 배열에 이름 board를 붙여 넘기고, 20번 줄은 인자 자리에 배열을 바로 새로 씁니다.
const board = [
{ name: '커피' },
{ name: '녹차' },
];
const a = low(board);
const b = low([{ name: '빵' }]);
console.log(a);
console.log(b);
검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? Node로 돌리면 무엇이 찍힐까요?
검사 결과와 실행 결과 보기
main.ts(19,15): error TS2345: Argument of type '{ name: string; }[]' is not assignable to parameter of type 'Priced[]'. Property 'price' is missing in type '{ name: string; }' but required in type 'Priced'.main.ts(20,18): error TS2353: Object literal may only specify known properties, and 'name' does not exist in type 'Priced'.{ name: '커피' }
{ name: '빵' }예상과 다르면 그곳이 배울 곳입니다. 19번 줄과 20번 줄이 다 막힙니다. 까닭은 같습니다 — 원소에 price가 없어 조건 Priced를 지나지 못합니다. 그런데 오류 문장이 다릅니다.
19번 줄의 TS2345는 '{ name: string; }[] 타입 인자는 Priced[] 타입 매개변수에 넣을 수 없다'는 뜻이고, 다음 줄이 까닭을 붙입니다 — 'price 속성이 { name: string; }에는 없는데 Priced에는 꼭 있어야 한다'. 조건을 지나지 못하면 검사기는 T 칸을 조건 Priced로 채우고 인자를 Priced[]에 맞춰 봅니다. 그래서 오류 문장에 Priced[]가 나옵니다.
20번 줄은 인자 자리에 객체를 바로 새로 썼습니다. 그러면 검사기는 Priced에 없는 name부터 짚습니다(새로 쓴 객체의 남는 속성). 문장은 name을 말해도 까닭은 빠진 price입니다.
검사 오류가 있어도 Node는 타입을 지우고 돌립니다(검사 규칙 1). 원소에 price가 없어 9번 줄의 견주기가 늘 false이니, best가 바뀌지 않고 첫 원소가 찍힙니다.
19번 줄은 지나가리라 봤다면 남는 속성과 빠진 속성을 섞은 것입니다. 이름을 거치면 남는 name은 짚지 않지만, 빠진 price는 이름을 거쳐도 짚습니다(검사 규칙 4).
예제
검사 결과와 실행 결과를 먼저 적어 본 뒤 펼쳐 보세요. 맞혀 보기는 점수에 들지 않습니다.
같은 low를 장보기 목록에도 씁니다. 주석 1 · 2 · 3은 제네릭 함수로 감싸는 차례이고, 앞 레슨에서 미뤄 둔 차례 2 '쓰는 속성은 extends로'를 여기서 붙였습니다.
interface Priced(2절과 같음)
interface Priced {
price: number;
}// 1. 바뀌는 타입을 T로
// 2. 쓰는 속성은 extends로
function low<T extends Priced>(
items: T[],
) {
let best = items[0];
for (const x of items) {
if (x.price < best.price) {
best = x;
}
}
// 3. 돌려주는 값은 T로
return best;
}
장보기 목록은 이름을 name이 아니라 item에 담았습니다.
const cart = [
{ item: '우유', price: 2500 },
{ item: '두부', price: 1800 },
{ item: '달걀', price: 5000 },
];
const c = low(cart);
console.log(c.item, c.price);
검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? Node로 돌리면 무엇이 찍힐까요?
검사 결과와 실행 결과 보기
두부 1800예상과 다르면 그곳이 배울 곳입니다. 검사는 오류 없이 지나갑니다. 23번 줄에서 cart의 원소는 item과 price를 가진 객체라 조건 Priced를 지나고, T 칸이 그 원소 타입 그대로 채워집니다. 그래서 c에 item이 남아 24번 줄의 c.item을 쓸 수 있습니다.
조건은 price만 봅니다. 이름이 name이든 item이든 상관없습니다. 실행하면 가장 싼 두부와 그 값이 찍힙니다.
24번 줄에서 막히리라 봤다면 T가 Priced로 줄어 item이 사라진다고 본 것입니다(조건 타입으로 줄어든다는 생각).
자주 틀리는 곳
틀린 생각: T는 무엇이든 될 수 있으니, 함수 안에서 T 값의 속성을 무엇이든 꺼낼 수 있다.
T가 아무 타입이나 받으니, price든 name이든 다 있을 것처럼 보입니다.
맞는 규칙: 조건 없는 T는 무엇으로든 채워질 수 있어, 검사기는 T 값에 어떤 속성도 있다고 보지 않습니다. 쓸 속성은 extends로 조건에 적어 둡니다(1절 · 검사 규칙 7).
틀린 생각: T extends Priced는 Priced를 상속한 타입만 받는다.
클래스에서 extends는 상속이었으니, 조건도 Priced라는 이름을 적어 둔 타입만 지날 것 같습니다.
맞는 규칙: 조건의 extends는 'Priced에 드는'이라는 뜻이고, 검사기는 모양으로 맞춥니다. Menu처럼 Priced를 적지 않은 타입도 price: number 줄이 있으면 듭니다(2절 · 검사 규칙 4).
틀린 생각: 조건을 지나면 T가 Priced로 줄어들어, 돌려받은 값에는 price만 남는다.
조건 타입이 Priced이니 T도 Priced가 될 것 같습니다.
맞는 규칙: T 칸은 넘긴 타입 그대로 채워집니다. low(menu)의 T는 Menu라 cheap에 name이 남습니다. 조건은 올 타입을 거를 뿐입니다(2절 · 검사 규칙 7).
틀린 생각: 남는 속성 오류(TS2353)가 나면 짚은 속성을 지우면 된다.
3절 20번 줄의 오류가 'name이 Priced에 없다'고 하니, name을 지우면 풀릴 것 같습니다.
맞는 규칙: 조건을 어긴 객체를 인자 자리에 바로 쓰면, 빠진 price보다 남는 name이 먼저 짚힙니다. 까닭은 빠진 price라 name을 지워도 price가 없으면 여전히 막힙니다. 빠진 속성을 채웁니다(3절).
확인 문제
1. 2절의 low에 장보기 목록 셋을 넘깁니다. 1~14번 줄은 2절과 같습니다. 검사가 막히는 줄은 몇 번일까요? 검사기가 a와 b의 T 칸을 무엇으로 채우는지도 적어 보세요.
interface Priced {
price: number;
}
function low<T extends Priced>(
items: T[],
) {
let best = items[0];
for (const x of items) {
if (x.price < best.price) {
best = x;
}
}
return best;
}
앞 코드에 이어 목록 셋을 넘깁니다.
const milk = [{ price: 2500 }];
const tofu = [{ won: 1800 }];
const eggs = [
{ price: 5000, ea: 10 },
];
const a = low(milk);
const b = low(eggs);
const c = low(tofu);
힌트
목록마다 원소 타입을 먼저 적고, 그 타입에 price: number 줄이 있는지 봅니다. 조건을 지나면 T 칸은 원소 타입 그대로 채워집니다.
정답
22번 줄이 막힙니다.
main.ts(22,15): error TS2345: Argument of type '{ won: number; }[]' is not assignable to parameter of type 'Priced[]'. Property 'price' is missing in type '{ won: number; }' but required in type 'Priced'.| 이름 | 조건 Priced | T · 돌려주는 타입 |
|---|---|---|
| a | 지남 | { price: number; } |
| b | 지남 | { price: number; ea: number; } |
b는 ea 줄이 더 있어도 조건을 지나고(검사 규칙 4), T 칸이 원소 타입 그대로 채워져 b에 ea가 남습니다. 22번 줄의 tofu는 원소에 price가 없어 조건을 지나지 못합니다. 이름을 거친 인자라 TS2345로 짚힙니다(3절).
흔한 오답은 b를 Priced로 적는 것입니다. 조건을 지나면 조건 타입이 된다고 본 것입니다(조건 타입으로 줄어든다는 생각). T는 넘긴 원소 타입입니다(검사 규칙 7).
2. 점수 목록에서 점수가 가장 높은 학생을 돌려주는 함수 high를 씁니다. 10번 줄에서 x.score를 쓸 수 있고 돌려받은 값에 학생 이름도 남도록, 5번 줄의 빈칸을 채우세요.
interface Scored {
score: number;
}
// 2. 쓰는 속성은 extends로
function high<____>(
items: T[],
) {
let best = items[0];
for (const x of items) {
if (x.score > best.score) {
best = x;
}
}
return best;
}
힌트
6번 줄이 T를 쓰니 5번 줄에서 T 칸을 만들어야 합니다. 그 칸에 score가 있는 타입만 오게 하는 조건을 함께 적습니다.
정답
빈칸은 T extends Scored입니다. 조건이 있어 10번 줄의 x.score를 쓸 수 있습니다. 돌려주는 타입이 T라, 이름과 점수를 가진 학생 목록을 넘기면 돌려받은 값도 그 학생 타입 그대로입니다. 그래서 .name을 쓸 수 있습니다.
다르게 써도 되는 곳: 조건 자리에 모양을 바로 적은 T extends { score: number }도 지나갑니다. Scored와 같은 모양이기 때문입니다(검사 규칙 4).
흔한 오답은 셋입니다.
T— T 칸은 생기지만 조건이 없어, 10번 줄이 TS2339로 두 번 막힙니다(T는 아무거나라는 생각).Scored— 조건이 아니라 Scored라는 이름의 새 타입 칸을 만듭니다. 6번 줄의 T가 어디에도 없어 TS2304('T라는 이름을 찾을 수 없다')가 납니다.T extends Scored[]— T가 원소 하나가 아니라 배열 전체를 가리킨다고 본 것입니다(인자 타입이 곧 T라는 생각). x가 배열이라 10번 줄이 TS2339로 막힙니다. 조건에는 원소 하나의 타입을 적습니다.
3. 빵과 쿠키 가운데 싼 것을 고르려는 코드가 막혔습니다. 검사 오류를 없애고, 실행하면 쿠키가 찍히게 하는 고치기를 고르세요. 1~14번 줄은 2절의 low 그대로입니다.
interface Priced {
price: number;
}
function low<T extends Priced>(
items: T[],
) {
let best = items[0];
for (const x of items) {
if (x.price < best.price) {
best = x;
}
}
return best;
}
앞 코드에 이어 빵과 쿠키를 넘깁니다.
const c = low([
{ name: '빵', won: 3000 },
{ name: '쿠키', won: 2000 },
]);
console.log(c.name);
main.ts(16,5): error TS2353: Object literal may only specify known properties, and 'name' does not exist in type 'Priced'.main.ts(17,5): error TS2353: Object literal may only specify known properties, and 'name' does not exist in type 'Priced'.main.ts(19,15): error TS2339: Property 'name' does not exist on type 'Priced'.- ① 16 · 17번 줄의
won을price로 바꾼다. - ② 16 · 17번 줄에서
name과 그 값을 지운다. - ③ low 머리의
extends Priced를 지운다.
힌트
16 · 17번 줄의 객체에 조건 Priced가 바라는 줄이 있는지 봅니다. 19번 줄의 c가 왜 Priced인지는 3절에 있습니다.
정답
①입니다. 원소에 price가 생겨 조건을 지나고, T 칸이 원소 타입 그대로 채워져 19번 줄의 c.name도 지나갑니다. 실행하면 2000이 더 싸니 쿠키가 찍힙니다.
- ② 오류가 짚은 name을 지운 것입니다(오류 줄이 고칠 줄이라는 생각). price는 여전히 없어 16 · 17번 줄이 이번에는 won을 남는 속성으로 짚고, 19번 줄도 막힙니다. 실행하면
undefined가 찍힙니다. - ③ 조건이 없으면 T 칸이 원소 타입 그대로라 19번 줄은 지나가지만, 9번 줄의
x.price와best.price가 TS2339로 막힙니다(1절). 실행하면 견주기가 늘 false라 첫 원소빵이 찍힙니다.
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
2절 코드를 main.ts로 저장하고, 4번 줄에서 extends Priced를 지웁니다. 오류가 몇 번 줄 몇 열에 몇 개 날지와 Node가 찍을 값을 먼저 적고, npx tsc로 검사한 뒤 node main.ts로 돌려 맞춰 봅니다. 편집기에서 cheap에 마우스를 올려 cheap의 타입도 확인합니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.