TS의 interface에 대해서

인덱스 시그니처에서 침몰했던 기억을 되살리며

Table Of Contents

들어가기

한번쯤 interface에 대해서 정리하고 넘어갈 필요가 있다고 느꼈다.

interface

TypeScript에서 객체 타입에 이름을 붙이기 위해서 사용할 수 있는 방법이다.

interface로 타입 선언하기

interface로 선언하는 타입의 이름은 관습적으로 대문자로 시작하는 것이 좋다.

interface Post { id: string; title: string; subTitle?: string; parent_id: string | null; type: string; sub_blog: string; created_at: string; tags: Tag[]; }

블로그를 만들면서 포스트의 타입을 선언했다.

객체 안의 속성과 속성을 구분하기 위해서는 콤마(,)나 세미콜론(;), 혹은 줄바꿈을 사용할 수 있다. 위 속성에서 세미콜론을 모두 콤마로 대체하거나, 줄바꿈만 사용해도 괜찮다.

하지만 만약 타입을 한 줄로 써야 한다면, 콤마나 세미콜론만 사용해야 한다.

interface Tag { text: string; isSpoiler: boolean }

이렇게 말이다.

함수 타이핑하기

interface Func { (x: number, y: number): number; } const add: Func = (x, y) => x + y;

interface로 함수 또한 타이핑할 수 있다.

interface 선언 병합(declaration merging)

컴파일러는 동일한 이름으로 선언된 두 개 이상의 인터페이스 선언을 하나의 선언으로 병합하게 된다. 이것을 선언 병합(declaration merging)이라고 부른다.

여기에서 선언이란 인터페이스, 네임스페이스(Namespace) 선언을 의미하지만, 우선 먼저 인터페이스가 병합되는 것을 먼저 보자.

아래처럼 같은 이름을 가지지만, 다른 속성을 가지는 인터페이스를 선언했다. 이 두 인터페이스는 컴파일러가 하나의 선언으로 병합하기 때문에, 맨 밑처럼 post를 선언해도 문제가 되지 않는다.

interface Post { title: string; } interface Post { subTitle: string; } const post: Post = { title: "제목", subTitle: "부제목", };

이런 식으로 다른 사람이 작성한 인터페이스를 확장해서 쓸 수 있다.

하지만 역으로 의도치 않게 다른 사람이 작성한 인터페이스와 병합되어 문제가 발생할 수도 있다. 이를 해결하기 위한 방법으로는 네임스페이스를 선언하는 방법이 있다.

namespace 선언하기

namespace MyNamespace { interface Post { title: string; } interface Post { subTitle: string; } const correct: Post = { title: "제목", subTitle: "부제목", }; }

이렇게 네임스페이스를 먼저 선언하고, 그 안에 인터페이스를 선언하면 된다.

네임스페이스 안에서는 correct처럼 Post를 이용해 변수를 선언할 수 있다.

단, 네임스페이스 밖에서 인터페이스를 사용하기를 원한다면, 이렇게 export를 통해서 인터페이스를 export해줘야 한다. 만약 같은 이름의 인터페이스가 2개 이상 있다면, 모두 다 export해줘야 한다.

namespace MyNamespace { export interface Post { title: string; } export interface Post { subTitle: string; } }

이렇게 선언한 다음에는 바깥에서 Post 인터페이스를 새롭게 선언해도 둘은 병합되지 않는다.

namespace MyNamespace { export interface Post { title: string; } export interface Post { subTitle: string; } } interface Post { content: string; } const correct1: MyNamespace.Post = { title: "제목", subTitle: "부제목", }; const correct2: Post = { content: "내용", };

네임스페이스에 접근할 때는 .을 사용해서 접근할 수 있다.

MyNamespace 안의 Post와 바깥의 Post는 병합되지 않으므로, 아래와 같은 선언은 오류가 발생한다.

// Type '{ title: string; subTitle: string; content: string; }' is not assignable to type 'Post'. // Object literal may only specify known properties, and 'content' does not exist in type 'Post'. const incorrect1: MyNamespace.Post = { title: "제목", subTitle: "부제목", content: "내용", }; // Type '{ title: string; subTitle: string; content: string; }' is not assignable to type 'Post'. // Object literal may only specify known properties, and 'title' does not exist in type 'Post'. const incorrect2: Post = { title: "제목", subTitle: "부제목", content: "내용", };

namespace 병합

위에서 언급했듯이, 선언이 병합되는 것은 인터페이스 뿐만이 아니다. 네임스페이스 역시 같은 이름을 가지고 있다면, 네임스페이스끼리도 선언이 병합된다.

interface 상속하기

extends 키워드를 사용하면 다른 인터페이스의 속성을 상속받을 수 있다.

interface Animal { type: string; } interface Dog extends Animal { breed: string; }

Animal 타입은 type 속성만 가지고 있지만, Dog 타입은 Animal타입이 가지고 있는 속성에 추가로 breed라는 속성을 지닌다.

인덱스 시그니처(Index Signature)

타입 선언 시에는 타입의 모든 속성 이름을 모를 수도 있다! 하지만 속성의 값들이 어떤 타입을 가지는지는 알고 있는 경우가 있다.

예를 들자면, ["one", "two", "three"] 같은 배열이 있다. arr[1]처럼 배열은 인덱싱하고 싶은데, 배열이 어디까지 길어질지 모르기 때문에 속성 이름을 함부로 지정할 수 없는 것이다.

이런 경우에는 인덱스 시그니처를 사용해 타입을 정의할 수 있다.

interface StringArray { [key: number]: string; } const arr: StringArray = ["one", "two", "three"]; const secondItem = arr[1];

[key: number]: string이란 객체의 속성 키가 전부 number이고, 해당 키로 접근한 속성의 타입이 string이라는 뜻이다.

예시 1

interface NumberDictionary { [index: string]: number; length: number; name: string; // Property 'name' of type 'string' is not assignable to 'string' index type 'number'. }

이렇게 NumberDictionary 타입을 선언한다면 오류가 발생한다.

예시 2

interface StringArray { [index: number]: string; length: number; name: string; }

그렇다면 이렇게 생긴 StringArray 타입은 어떨까?

interface StringArray { [index: number]: string; length: number; name: string; 3: number; // Property '3' of type 'number' is not assignable to 'number' index type 'string'. }

이렇게 타입을 수정해보았다.

참고

타입스크립트 교과서(조현영 저)

TypeScript Documentation - Declaration Merging

TypeScript Documentation - Objectjs