interface의 extends와 타입 별칭의 교차(&)는 이미 있는 객체 타입에 새 속성 줄을 더해 확장된 타입을 만드는 문법입니다. 공통 속성을 한 번만 적어 두고 음료나 디저트처럼 종류마다 필요한 줄만 덧붙이면, 원래 타입과 어긋나지 않게 새 타입을 만들 수 있습니다.
카페 메뉴마다 이름(name)과 가격(price)이 공통으로 들어가고, 음료에만 샷 수(shots)가 더 붙는다고 합시다. 똑같은 두 줄을 매번 다시 적으면 나중에 속성 이름이 바뀔 때 여러 곳을 함께 고쳐야 합니다. 이 레슨에서는 있는 모양에 줄을 더하는 두 가지 표기와, 모음을 합치는 유니언(|)과 모음이 겹치는 교차(&)의 차이를 살펴봅니다.
JS 다시 보기: 실행 때 객체의 속성 이름을 배열로 꺼내는 Object.keys(객체 돌기), 값의 종류를 글자로 돌려주는 typeof(수·글자·불리언과 typeof), 클래스 상속의 extends(상속: extends와 super)를 알고 있으면 이 단원의 타입 전용 문법과 비교하기 쉽습니다.
이 단원을 이미 안다면 단원 연습 문제의 진단 문항부터 풀고 틀린 레슨만 보세요.
1interface extends와 교차(&)로 속성 줄 합치기
이름과 가격 두 줄을 가진 MenuItem에 shots: number 줄을 더해 Drink를 만듭니다. interface에서는 extends MenuItem을 쓰고, type 별칭에서는 &(교차, intersection) 기호로 두 모양을 이어 붙입니다.
interface MenuItem {
name: string;
price: number;
}
interface Drink
extends MenuItem
{
shots: number;
}
type Ade = MenuItem & {
ice: boolean;
};
앞 코드에 이어 Drink와 Ade 자리에 값을 넣어 봅니다.
const d: Drink = {
name: '라테',
price: 4500,
shots: 2,
};
const a: Ade = {
name: '레몬에이드',
price: 5000,
ice: true,
};
console.log(d.shots, a.ice);
2 trueDrink는 MenuItem의 두 줄(name, price)과 새로 적은 shots 줄을 합쳐 모두 세 줄을 요구합니다. Ade도 MenuItem의 두 줄과 ice 줄을 합쳐 세 줄을 요구합니다.
만약 합쳐진 줄 가운데 하나라도 빼먹으면 어떻게 될까요? shots 줄을 빼고 Drink 자리에 넣어 봅니다.
interface MenuItem {
name: string;
price: number;
}
interface Drink
extends MenuItem
{
shots: number;
}
const latte: Drink = {
name: '라테',
price: 4500,
};
main.ts(11,7): error TS2741: Property 'shots' is missing in type '{ name: string; price: number; }' but required in type 'Drink'.11번 줄에서 Drink에 꼭 있어야 하는 shots 속성이 빠졌다는 TS2741 오류가 납니다. 거꾸로 MenuItem을 받는 함수에는 세 줄을 다 가진 Drink 값을 그대로 넘길 수 있습니다(모양으로 맞추기 — 검사 규칙 4).
직접 만드는 확인 함수에서 'item' in v를 지난 뒤 검사기가 보여 준 칩을 떠올려 보세요. 그 칩은 앞쪽 object & 뒤에 Record<"item", unknown>이 붙은 꼴이었습니다. 이제 앞쪽의 object &를 읽을 수 있습니다 — '원래 object 모양이면서 동시에 뒤쪽 속성 줄까지 함께 가진 값'이라는 뜻입니다(뒤쪽 Record는 이 단원 마지막 레슨 Record — 키 타입으로 만드는 객체 타입에서 마저 읽습니다).
2유니언(|)과 교차(&)를 모음으로 비교하기
유니언 — 여럿 가운데 하나의 A | B와 이 레슨의 A & B는 기호 하나 차이지만, 값의 모음과 확실히 꺼낼 수 있는 속성 줄이 정반대로 움직입니다.
interface Named {
name: string;
}
interface Priced {
price: number;
}
type Either = Named | Priced;
type Both = Named & Priced;
앞 코드에 이어 두 타입 자리에 값을 넣고 속성을 읽어 봅니다. 몇 번 줄이 막힐지 먼저 생각해 본 뒤 펼쳐 보세요.
const u: Either = {
name: '쿠키',
};
const i: Both = {
name: '쿠키',
price: 2500,
};
console.log(i.name, i.price);
console.log(u.price);
검사 결과 보기
main.ts(17,15): error TS2339: Property 'price' does not exist on type 'Named'.예상과 다르면 그곳이 배울 곳입니다.
9~11번 줄의 u: Either에는 name 한 줄만 있는 객체도 들어갑니다. 하지만 꺼내 쓸 때는 Named 쪽에 price가 없어 17번 줄 u.price가 TS2339로 막힙니다.
반대로 12~15번 줄의 i: Both에는 name과 price 두 줄을 모두 갖춘 객체만 들어갈 수 있고, 들어온 뒤에는 16번 줄처럼 두 속성을 모두 안전하게 꺼낼 수 있습니다.
| 구분 | 유니언 (Named | Priced) | 교차 (Named & Priced) |
|---|---|---|
| 뜻 | 둘 가운데 하나 이상 | 둘 다 동시에 만족 |
| 들어가는 값 | 많아짐 (한쪽 줄만 있어도 됨) | 적어짐 (두 쪽 줄이 다 있어야 함) |
| 확실한 속성 줄 | 적어짐 (둘이 함께 가진 줄만) | 많아짐 (두 쪽의 줄을 합침) |
예제
검사 결과와 실행 결과를 먼저 적어 본 뒤 펼쳐 보세요. 맞혀 보기는 점수에 들지 않습니다.
기본 메뉴 MenuItem에 포장 요금(box: number) 줄을 더한 PackedItem을 만들고, 합계 금액을 구하는 함수를 부릅니다.
interface MenuItem {
name: string;
price: number;
}
type PackedItem = MenuItem & {
box: number;
};
function total(p: PackedItem) {
return p.price + p.box;
}
const cake: PackedItem = {
name: '케이크',
price: 6000,
box: 500,
};
console.log(
cake.name,
total(cake),
);
검사를 지나갈까요? Node로 돌리면 무엇이 찍힐까요?
검사 결과와 실행 결과 보기
케이크 6500예상과 다르면 그곳이 배울 곳입니다. cake는 MenuItem의 name, price와 덧붙인 box 세 줄을 모두 갖추었으므로 검사를 지나고, 실행하면 케이크 6500이 찍힙니다.
자주 틀리는 곳
틀린 생각: Drink에 extends MenuItem을 적으면 실행할 때도 부모 객체의 기본값을 물려받는다.
자바스크립트의 class Child extends Parent(상속: extends와 super · 클래스에 타입 붙이기)와 똑같은 단어 extends를 쓰니, 실행할 때도 무언가를 물려줄 것처럼 보입니다(타입이 실행 때 남는다는 생각).
맞는 규칙: interface의 extends는 검사기가 확인할 속성 줄만 합칠 뿐, 실행 전에 통째로 지워집니다(검사 규칙 1 · 4). 실제 속성 값은 객체를 만들 때 직접 채워 넣어야 합니다.
틀린 생각: Named & Priced는 두 타입을 더했으니 Named만 있는 값도 들어가고 Priced만 있는 값도 들어가는 더 넓은 타입이다.
일상 말에서 'A와 B'라고 하면 A 모음과 B 모음을 합친 것처럼 들리기 쉽습니다.
interface Named {
name: string;
}
interface Priced {
price: number;
}
type Both = Named & Priced;
const item: Both = {
name: '라테',
};
main.ts(9,7): error TS2322: Type '{ name: string; }' is not assignable to type 'Both'. Property 'price' is missing in type '{ name: string; }' but required in type 'Priced'.맞는 규칙: 타입에서 &는 두 조건이 겹치는 곳(교집합)입니다. 두 모양의 속성 줄을 모두 가진 값만 들어갈 수 있어 값의 모음은 더 좁아지고, 보장되는 속성 줄은 늘어납니다(2절 표).
틀린 생각: 두 객체 타입의 속성을 모두 쓰려고 할 때 Named | Priced처럼 |로 적는다.
'이 속성도 쓰고 저 속성도 쓴다'를 |로 잘못 적으면, 넣을 때는 한쪽만 있어도 통과하지만 정작 속성을 꺼내 쓸 때 TS2339로 막힙니다.
맞는 규칙: '둘 가운데 어느 한쪽'이면 유니언(|)을 쓰고, '두 모양의 줄을 모두 합친 하나'이면 교차(&)나 interface ... extends를 씁니다(2절).
확인 문제
1. 다음 코드는 검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 어떤 이유로 막힐까요?
interface Book {
title: string;
}
interface EBook extends Book {
sizeMb: number;
}
const b: EBook = {
title: '어린 왕자',
};
힌트
EBook이 Book에서 이어받은 줄과 새로 더한 줄을 합쳐 모두 몇 줄인지 세어 봅니다.
정답
main.ts(8,7): error TS2741: Property 'sizeMb' is missing in type '{ title: string; }' but required in type 'EBook'.8번 줄에서 TS2741로 막힙니다. EBook은 title과 sizeMb 두 줄을 모두 요구하는데, b에 sizeMb 줄이 빠졌습니다.
흔한 오답은 '지나간다'입니다. Book의 title 줄만 있어도 된다고 본 것입니다.
2. Person 타입과 { grade: number }를 합쳐 name과 grade 두 줄을 모두 가지는 Student 타입을 만들려고 합니다. 4번 줄 빈칸에 알맞은 기호를 채우세요.
interface Person {
name: string;
}
type Student = Person __ {
grade: number;
};
const s: Student = {
name: '민지',
grade: 2,
};
console.log(s.name, s.grade);
힌트
두 객체 모양의 속성 줄을 모두 갖추도록 합치는 교차 기호입니다(1절 · 2절).
정답
빈칸은 &입니다.
interface Person {
name: string;
}
type Student = Person & {
grade: number;
};
const s: Student = {
name: '민지',
grade: 2,
};
console.log(s.name, s.grade);민지 2다르게 써도 되는 곳: type 대신 interface로 Student extends Person을 선언하고 grade: number; 줄을 적어도 똑같이 동작합니다.
흔한 오답은 |입니다. |로 적으면 s.name이나 s.grade를 읽을 때 한쪽 타입에 없는 속성이라며 TS2339가 납니다.
3. 운동 기록을 다루는 아래 코드에서 13번 줄이 TS2339로 막힙니다. 13번 줄이 검사를 지나고 달리기 30을 찍게 하려면 7번 줄을 어떻게 고쳐야 할까요?
interface Sport {
title: string;
}
interface Timed {
minutes: number;
}
type Log = Sport | Timed;
function printLog(l: Log) {
console.log(
l.title,
l.minutes,
);
}
main.ts(11,7): error TS2339: Property 'title' does not exist on type 'Log'. Property 'title' does not exist on type 'Timed'.main.ts(12,7): error TS2339: Property 'minutes' does not exist on type 'Log'. Property 'minutes' does not exist on type 'Sport'.- ① 7번 줄을
type Log = Sport & Timed;로 바꾼다. - ② 7번 줄을
type Log = Sport;로 바꾼다. - ③ 9번 줄을
function printLog(l: Timed)로 바꾼다.
힌트
printLog 함수는 title과 minutes 두 속성을 모두 꺼내 씁니다. 두 모양의 줄을 모두 요구하는 기호를 고릅니다.
정답
①입니다. Sport & Timed로 바꾸면 Log가 title과 minutes 두 줄을 모두 가진 타입이 되어 11·12번 줄 오류가 모두 사라집니다.
- ②
Sport만 남기면l.minutes가 여전히TS2339로 막힙니다. - ③
Timed만 남기면l.title이 여전히TS2339로 막힙니다.
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
1절 첫 코드의 d: Drink에서 shots: 2, 줄을 지우고 npx tsc로 검사해 11번 줄에서 TS2741이 나는지 확인해 보세요. 다음 레슨은 keyof와 속성 타입 꺼내기입니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.