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이라는 뜻이다.

  • 인덱스 시그니처의 key 타입으로는 일부 타입 (string, number, symbol, 템플릿 문자열 패턴, 그리고 이들의 유니온 타입)만 가능하다.

예시 1

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

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

  • 첫줄에서 index의 타입이 string이면 해당하는 속성의 타입은 number라고 정의했는데, 아래에서 name, 즉 string 타입으로 인덱싱을 하면 이에 해당하는 값의 타입은 항상 number여야 하는데, string 타입이라고 했으므로 오류가 발생하는 것이다.
  • lengthstring 타입으로 인덱싱을 하면서, 이에 해당하는 값의 타입이 number이므로 첫째줄과 충돌하지 않는 것이다.

예시 2

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

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

  • number 타입으로 인덱싱을 하는 경우, 그에 해당하는 값의 타입은 string이어야 한다.
  • lengthname 모두 string 타입이므로, 이들로 인덱싱하는 경우와 충돌하는 선언은 없다 따라서 오류는 발생하지 않는다.
interface StringArray { [index: number]: string; length: number; name: string; 3: number; // Property '3' of type 'number' is not assignable to 'number' index type 'string'. }

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

  • 이 경우, 3은 number 타입이기 때문에 StringArray[3]처럼 인덱싱하는 경우, 이에 해당하는 값의 타입이 number여야 한다고 주장하고 있는 것이다. 따라서 첫째줄과 충돌하기 때문에 오류가 발생한다.

참고


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

TypeScript Documentation - Declaration Merging

TypeScript Documentation - Objectjs