객체 타입은 그 속성 줄을 가진 값의 모음입니다. 그래서 검사기는 객체를 넣을 때 타입의 이름을 보지 않고 모양을 맞춥니다. 필요한 줄을 다 가진 값이면 이름이 다른 타입이어도, 줄이 더 있어도 그 자리에 들어갑니다.
자바스크립트에서는 객체를 넘길 때 그 객체를 누가 어떻게 만들었는지 묻지 않습니다. 함수가 쓰는 속성만 있으면 돕니다. 타입스크립트의 검사기도 이 습관을 그대로 따릅니다. 이 규칙을 모르면 이름이 다른 두 타입이 왜 섞이는지, 줄이 더 있는 객체가 왜 들어가는지 헷갈립니다. 이 레슨에서 검사 규칙 4를 세웁니다.
1Cup 자리에 드는 값 — 줄을 가진 값의 모음
1단원에서 타입은 그 자리에 올 수 있는 값의 모음이라고 했습니다(검사 규칙 2). number는 모든 수의 모음입니다.
객체 타입도 모음입니다. 아래 Cup은 'string 타입 size 줄을 가진 객체'를 모두 모은 것입니다. 세 객체를 Cup 자리에 넣어 봅니다.
interface Cup {
size: string;
}
const a: Cup = { size: 'tall' };
const b = {
size: 'grande',
shot: 2,
};
const c: Cup = b;
const d = { shot: 2 };
const e: Cup = d;
console.log(a.size, c.size);
main.ts(12,7): error TS2741: Property 'size' is missing in type '{ shot: number; }' but required in type 'Cup'.tall grande5번 줄의 객체는 size 줄을 가져 Cup 자리에 들어갑니다. 6~9번 줄의 b는 size 줄에 shot 줄이 더 있습니다. 그래도 10번 줄에서 Cup 자리에 들어갑니다. size 줄을 가진 객체라는 조건을 채웠기 때문입니다. 11번 줄의 d에는 size 줄이 없어 Cup의 모음 밖입니다. 12번 줄은 앞 레슨에서 본 TS2741, 곧 'size 속성이 빠졌다'로 막힙니다.
| 객체 | 가진 줄 | Cup 자리에 |
|---|---|---|
{ size: 'tall' } | size | 든다 |
| b | size · shot | 든다 — 줄이 더 있어도 |
| d | shot | 안 든다 — size 줄이 없다 |
모양 그림으로 보면, Cup의 모양은 이 모음을 펼친 것입니다. 'size 줄을 가졌는가'가 곧 '이 모음에 드는가'입니다.
2이름이 아니라 모양으로 맞춘다
이번에는 줄이 똑같은 interface 둘을 이름만 다르게 만들었습니다. Box 타입으로 만든 box를 Cup 자리에 넣습니다. 11번 줄은 검사를 지나갈까요? 답을 정한 뒤 펼쳐 보세요.
interface Box {
size: string;
}
interface Cup {
size: string;
}
const box: Box = {
size: 'tall',
};
const cup: Cup = box;
console.log(cup.size);
검사 결과와 실행 결과 보기
tall예상과 다르면 그곳이 배울 곳입니다. 검사 결과에 오류가 없습니다. 11번 줄도 지나갔습니다. 검사기는 11번 줄에서 box의 타입 이름 Box와 Cup을 견주지 않습니다. Cup이 바라는 size 줄이 box에 있는지만 봅니다. 있으니 지나갑니다.
검사 규칙 4. 객체 타입은 그 속성 줄을 가진 값의 모음입니다(검사 규칙 2). 그래서 검사기는 이름이 아니라 모양으로 맞춥니다 — 필요한 줄을 다 가진 값이면 타입 이름이 달라도, 줄이 더 있어도 듭니다.
자료에 따라 이렇게 모양으로 맞추는 것을 구조적 타이핑(structural typing)이라고 부릅니다.
모양으로 맞추는 방향을 봅니다. 넣는 쪽(box)은 받는 쪽(Cup)이 바라는 줄을 다 가져야 합니다. 거꾸로 받는 쪽이 모르는 줄이 넣는 쪽에 더 있는 것은 괜찮습니다. 다만 새로 쓴 객체를 그 자리에 바로 쓸 때는 남는 줄을 따로 잡습니다. 이것은 다음 레슨 '새로 쓴 객체의 남는 속성'에서 봅니다.
예제
줄이 더 있는 주문을 함수에 넘기기
serve는 Cup을 받는 함수입니다. 주문 객체에는 size 말고 shot 줄도 있습니다.
interface Cup {
size: string;
}
function serve(cup: Cup) {
console.log(cup.size, '한 잔');
}
const order = {
size: 'tall',
shot: 2,
};
serve(order);
tall 한 잔인자를 넘기는 것도 Cup 자리에 넣는 일이라 같은 규칙으로 맞춥니다(검사 규칙 4). order는 size 줄을 가져 지나갑니다.
serve 안에서는 cup의 선언 타입이 Cup이라 size 줄만 쓸 수 있습니다. 넘긴 객체에 shot이 있어도 함수 안에서 cup.shot을 꺼내면 검사기가 짚습니다(TS2339).
자주 틀리는 곳
틀린 생각: 타입 이름이 다르면 검사기가 두 타입을 섞지 못하게 막아 준다.
Won과 Cups처럼 이름을 다르게 지으면, 금액 자리에 잔 수를 넣는 실수도 검사기가 잡아 줄 것 같습니다.
interface Won {
amount: number;
}
interface Cups {
amount: number;
}
function pay(price: Won) {
const won = price.amount;
console.log(`${won}원`);
}
const today: Cups = {
amount: 3,
};
pay(today);
3원잔 수 3이 금액 자리에 들어가 3원이 찍혔습니다. Won과 Cups는 이름만 다르고 모양이 같아, 검사기에게는 같은 모음입니다. 검사를 지났지만 결과는 뜻과 다릅니다.
맞는 규칙: 검사기는 타입 이름이 아니라 줄만 보므로, 모양이 같은 두 타입은 서로의 자리에 들어갑니다. 뜻이 다른 값은 won · cups처럼 속성 이름부터 다르게 지어 모양을 가릅니다(검사 규칙 4).
틀린 생각: 줄이 더 있는 객체는 어디서나 그 타입 자리에 넣을 수 없다.
1절에서 shot 줄이 더 있는 b가 Cup 자리에 들어갔습니다. 그런데 같은 객체를 Cup 자리에 바로 쓰면 막힙니다.
interface Cup {
size: string;
}
const extra = {
size: 'tall',
shot: 2,
};
const a: Cup = extra;
const b: Cup = {
size: 'tall',
shot: 2,
};
main.ts(12,3): error TS2353: Object literal may only specify known properties, and 'shot' does not exist in type 'Cup'.이름 extra를 거친 9번 줄은 지나가고, 객체를 Cup 자리에 바로 쓴 10~13번 줄만 남는 줄 shot을 짚습니다. 남는 줄을 모두 막는 규칙이 아니라, 새로 쓴 객체에만 얹은 오타 잡기입니다. 자세한 것은 다음 레슨에서 봅니다.
맞는 규칙: 줄이 더 있는 값도 Cup의 모음에 들어서, 이름을 거쳐 넣으면 지나갑니다. 남는 줄은 객체를 그 자리에 새로 쓸 때만 오타로 잡힙니다(검사 규칙 4).
확인 문제
1. 2절 코드의 Cup에 shot: number; 줄을 더했습니다. const cup: Cup = box; 줄은 검사를 지나갈까요? 막힌다면 어떤 오류일까요?
interface Box {
size: string;
}
interface Cup {
size: string;
shot: number;
}
const box: Box = {
size: 'tall',
};
const cup: Cup = box;
console.log(cup.size);
힌트
넣는 쪽(box)이 받는 쪽(Cup)이 바라는 줄을 다 가졌는지 봅니다.
정답
막힙니다. 줄을 하나 더해 12번 줄이 된 그 줄입니다.
main.ts(12,7): error TS2741: Property 'shot' is missing in type 'Box' but required in type 'Cup'.Cup이 바라는 shot 줄이 Box에는 없습니다. 이번에는 문장에 새로 쓴 객체의 긴 타입 대신 이름 Box가 찍혔습니다. box의 선언 타입이 Box이기 때문입니다.
흔한 오답은 '지나간다'입니다. 줄이 더 있어도 괜찮다는 규칙을 거꾸로 읽은 것입니다. 줄이 더 있어도 되는 쪽은 넣는 쪽이고, 받는 쪽이 바라는 줄은 넣는 쪽에 다 있어야 합니다.
2. 빈칸에 속성 줄 하나를 써서 11번 줄이 검사를 지나게 하세요. 다 채운 코드는 true를 찍어야 합니다.
interface Cup {
size: string;
hot: boolean;
}
const drink = {
size: 'tall',
________
shot: 2,
};
const cup: Cup = drink;
console.log(cup.hot);
힌트
Cup이 바라는 줄 가운데 drink에 없는 줄을 찾습니다. 그 줄의 타입도 맞춥니다.
정답
hot: true,입니다.
interface Cup {
size: string;
hot: boolean;
}
const drink = {
size: 'tall',
hot: true,
shot: 2,
};
const cup: Cup = drink;
console.log(cup.hot);true다르게 써도 되는 곳: 줄의 차례는 상관없습니다. shot 줄은 Cup에 없지만 이름 drink를 거쳐 넣으므로 그대로 둬도 지나갑니다.
흔한 오답은 hot: 'yes',처럼 글자를 넣는 것입니다. 그러면 11번 줄에서 TS2322가 나고, 들여 쓴 설명 줄이 hot 속성의 타입이 맞지 않는다고 짚습니다.
3. 장보기 목록을 찍는 코드가 14번 줄에서 검사에 막힙니다. 검사 오류를 없애면서 달걀 10개가 찍히게 하는 고치기는 어느 것일까요?
interface Item {
name: string;
count: number;
}
function line(i: Item) {
const n = `${i.count}개`;
return `${i.name} ${n}`;
}
const egg = {
name: '달걀',
price: 300,
};
console.log(line(egg));
main.ts(14,18): error TS2741: Property 'count' is missing in type '{ name: string; price: number; }' but required in type 'Item'.플레이그라운드(타입스크립트 6.0)에서는 같은 14번 줄에 TS2345가 먼저 찍히고, 위 문장은 그 아래 들여 쓴 설명 줄로 나옵니다.
- ① egg에
count: 10,줄을 더한다. - ② egg의
price: 300,줄을 지운다. - ③ 1번 줄의 interface 이름 Item을 Egg로 바꾸고, 6번 줄도
i: Egg로 바꾼다.
힌트
오류 문장이 빠졌다고 짚는 속성을 봅니다. 검사기가 이름을 보는지 줄을 보는지 떠올립니다.
정답
①입니다.
interface Item {
name: string;
count: number;
}
function line(i: Item) {
const n = `${i.count}개`;
return `${i.name} ${n}`;
}
const egg = {
name: '달걀',
price: 300,
count: 10,
};
console.log(line(egg));달걀 10개Item이 바라는 count 줄이 생겨 지나갑니다. price 줄은 Item에 없지만 이름 egg를 거쳐 넘기므로 남아도 됩니다.
②는 남는 줄을 지울 뿐 빠진 count 줄은 그대로라 같은 오류가 납니다. ③은 이름만 바꿔 줄은 그대로라 역시 막힙니다(이름이 같아야 맞는다는 생각).
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
2절 코드를 main.ts로 저장해 npx tsc로 검사하고 node main.ts로 돌립니다. 그다음 Box에 shot: number; 줄을 더하면 어느 줄이 막힐지 먼저 적고, 다시 검사해서 맞춰 봅니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.