본문으로 건너뛰기

타입에 줄 더하기 — extends와 &

타입스크립트 목차타입으로 타입 만들기레슨 36 / 51

목차

interface의 extends와 타입 별칭의 교차(&)는 이미 있는 객체 타입에 새 속성 줄을 더해 확장된 타입을 만드는 문법입니다. 공통 속성을 한 번만 적어 두고 음료나 디저트처럼 종류마다 필요한 줄만 덧붙이면, 원래 타입과 어긋나지 않게 새 타입을 만들 수 있습니다.

카페 메뉴마다 이름(name)과 가격(price)이 공통으로 들어가고, 음료에만 샷 수(shots)가 더 붙는다고 합시다. 똑같은 두 줄을 매번 다시 적으면 나중에 속성 이름이 바뀔 때 여러 곳을 함께 고쳐야 합니다. 이 레슨에서는 있는 모양에 줄을 더하는 두 가지 표기와, 모음을 합치는 유니언(|)과 모음이 겹치는 교차(&)의 차이를 살펴봅니다.

JS 다시 보기: 실행 때 객체의 속성 이름을 배열로 꺼내는 Object.keys(객체 돌기), 값의 종류를 글자로 돌려주는 typeof(수·글자·불리언과 typeof), 클래스 상속의 extends(상속: extends와 super)를 알고 있으면 이 단원의 타입 전용 문법과 비교하기 쉽습니다.

이 단원을 이미 안다면 단원 연습 문제의 진단 문항부터 풀고 틀린 레슨만 보세요.

4단계 중 1단계. 1~4번 줄: MenuItem은 name과 price 두 줄을 가진 모양입니다.

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 true

Drink는 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,
};
검사 결과 오류 1개
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);
검사 결과 보기
검사 결과 오류 1개
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: '라테',
};
검사 결과 오류 1개
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에서 이어받은 줄과 새로 더한 줄을 합쳐 모두 몇 줄인지 세어 봅니다.

정답
검사 결과 오류 1개
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,
  );
}
검사 결과 오류 2개
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와 속성 타입 꺼내기입니다. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

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

타입스크립트 목차로