interface Child extends Parent는 Parent의 모든 멤버와 자신의 멤버를 가진 새 인터페이스를 만듭니다. 자식 타입의 값은 부모의 모든 속성을 가지므로 부모가 기대되는 곳 어디서든 쓸 수 있습니다.
같은 키워드가 TypeScript의 네 곳에서 서로 관련되지만 다른 의미로 나타납니다.
| 위치 | 예 | 의미 |
|---|---|---|
| 인터페이스 | interface Dog extends Animal {} | 부모의 멤버를 상속 |
| 클래스 | class Dog extends Animal {} | 필드와 메서드 구현을 상속(런타임 JavaScript) |
| 제네릭 매개변수 | <T extends Animal> | 제약: 타입 인수가 Animal에 대입 가능해야 함 |
| 조건부 타입 | T extends string ? A : B | 검사: T가 string에 대입 가능한가? |
여러 인터페이스 확장하기
extends 뒤에 부모 여러 개를 쉼표로 구분해 나열합니다. 결과는 그 모두의 멤버를 갖습니다.
interface Identified {
id: number;
}
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface Post extends Identified, Timestamped {
title: string;
}
// Post requires id, createdAt, updatedAt and title
두 부모가 같은 속성을 동일하지 않은 타입으로 선언하면, 자식이 그 속성을 두 타입 모두에 대입 가능한 타입으로 다시 선언하지 않는 한 자식은 오류 TS2320(Interface 'Post' cannot simultaneously extend types 'A' and 'B')입니다.
타입 별칭 확장하기
type 별칭에는 extends 절이 없습니다. 양쪽의 멤버를 합치는 교차 타입 &로 그 위에 쌓습니다. 인터페이스는 객체 타입 별칭을 직접 확장할 수도 있고, 타입 별칭은 인터페이스와 교차할 수 있습니다. 두 종류는 자유롭게 섞입니다.
두 방법은 속성이 충돌하기 전까지는 똑같이 동작합니다. extends는 충돌하는 속성을 선언에서 보고하지만, &는 조용히 never로 만듭니다. 비교는 interface와 type 비교 페이지에, &의 세부 사항은 교차 타입 페이지에 있습니다.
속성 타입 재정의하기
자식 인터페이스는 부모 속성을 다시 선언할 수 있지만, 부모의 타입에 대입 가능한 타입으로만 가능합니다. string을 리터럴로 좁히거나 유니언에서 null을 없애는 것은 허용됩니다. 관련 없는 타입으로 바꾸는 것은 안 됩니다.
index.ts(13,11): error TS2430: Interface 'Label' incorrectly extends interface 'Shape'.
Types of property 'kind' are incompatible.
Type 'number' is not assignable to type 'string'.
이 규칙은 Label을 Shape가 기대되는 모든 곳에서 여전히 쓸 수 있도록 하기 위해 존재합니다. kind가 숫자일 수 있다면, Shape를 위해 작성된 shape.kind.toUpperCase()를 호출하는 코드가 깨질 것입니다.
Omit으로 속성 바꾸기
상속받은 속성에 정말로 다른 타입이 필요하다면, 먼저 Omit으로 부모에서 그 속성을 제거한 다음 새 속성을 선언하세요. id와 날짜가 문자열로 도착하는 API 데이터를 변환하는 경우가 흔한 예입니다.
Omit<ApiUser, "id" | "createdAt">는 { name: string }이므로 새 id와 createdAt은 아무것과도 충돌하지 않습니다. Pick, Partial 등 다른 유틸리티 타입도 같은 방식으로 확장할 수 있습니다.
클래스의 extends
class Dog extends Animal은 JavaScript의 클래스 상속이며, 인터페이스의 extends와 달리 런타임에 존재합니다. Dog는 Animal의 생성자, 필드, 메서드 구현을 받고, new Dog(...) instanceof Animal은 true입니다. TypeScript는 그 위에 override 키워드 같은 타입 검사를 더합니다.
클래스는 클래스 하나만 extends할 수 있지만, 인터페이스는 몇 개든 implements할 수 있습니다. implements는 아무것도 복사하지 않고, 인터페이스가 요구하는 것을 클래스가 선언했는지만 검사합니다. super, protected 멤버, override는 상속에서 다룹니다.
제네릭과 조건부 타입의 extends
꺾쇠괄호 안에서 extends는 제약입니다. <T extends { length: number }>는 숫자 length를 가진 어떤 타입 인수든 받고, 함수 본문에서 .length를 쓸 수 있게 합니다. 조건부 타입에서 T extends U ? X : Y는 T가 U에 대입 가능한지 묻습니다.
두 곳 모두에서 extends는 인터페이스 형태가 검사하는 것과 같은 관계인 "대입 가능하다"를 뜻합니다. 제약은 제네릭 제약에서, 타입 수준의 검사는 조건부 타입에서 자세히 다룹니다.
자주 묻는 질문
TypeScript에서 인터페이스는 어떻게 확장하나요?
interface Child extends Parent { ... }라고 씁니다. 자식은 부모의 모든 속성과 자신이 선언한 속성을 갖습니다. 여러 인터페이스를 한 번에 확장하려면 쉼표로 구분하세요: interface C extends A, B { ... }.
TypeScript에서 타입 별칭은 어떻게 확장하나요?
타입 별칭은 extends를 쓸 수 없으므로 대신 교차 타입으로 합칩니다: type Admin = User & { permissions: string[] }. 인터페이스는 객체 타입 별칭을 직접 확장할 수도 있습니다: interface Admin extends User { permissions: string[] }.
인터페이스를 확장할 때 속성 타입을 재정의할 수 있나요?
호환되는 더 좁은 타입으로만 가능합니다. kind: "circle"은 kind: string을 대체할 수 있습니다. string 속성에 number처럼 다른 타입을 쓰면 오류 TS2430(Interface 'X' incorrectly extends interface 'Y')입니다. 속성을 관련 없는 타입으로 바꾸려면 먼저 제거하세요: interface User extends Omit<ApiUser, "id"> { id: number }.
extends와 implements의 차이는 무엇인가요?
extends는 상속합니다. 인터페이스는 부모의 멤버를 받고, 클래스는 부모 클래스의 필드와 메서드 구현을 받습니다. implements는 검사만 합니다. 인터페이스를 구현하는 클래스는 모든 멤버를 직접 선언해야 하고, 인터페이스로부터 얻는 것은 없습니다.
<T extends string> 같은 제네릭에서 extends는 무슨 뜻인가요?
상속이 아니라 제약입니다. T extends string은 타입 인수가 string에 대입 가능해야 한다는 뜻이므로, 함수 안에서 T에 문자열 메서드를 쓸 수 있습니다. 조건부 타입에서 T extends U ? X : Y는 검사입니다. T가 U에 대입 가능하면 결과는 X, 아니면 Y입니다.