저번 글에 이어 인터페이스에 대한 내용을 다뤄보자.
타입스크립트 핸드북(타입스크립트 핸드북)을 보고 공부한 것을 정리한 글입니다.
기본적인 JS 지식은 가진 상태라고 가정합니다.
핸드북에서는 인터페이스를 다음과 같이 정의했다.
인터페이스는 상호 간에 정의한 약속 혹은 규칙을 의미한다. 타입스크립트에서의 인터페이스는 보통 다음과 같은 범주에 대해 약속을 정의할 수 있다.
그에 따른 약속은 다음과 같다.
이게 무슨 말인가 하면, 위의 다섯 범주가 어떤 타입의 값들을 가지는지 명시해주는 도구가 바로 인터페이스인 것이다.
다음 예제를 살펴보자.
let person = { name: 'Capt', age: 28 };
function logAge(obj: { age: number }) {
console.log(obj.age); // 28
}
logAge(person); // 28
위 logAge()
함수에서 받는 인자의 형태는 age
를 속성으로 갖는 객체이다. 이처럼 인자를 받을 때 단순 타입 뿐 아니라 객체의 속성 타입까지 정의할 수 있다.
즉, 파라미터의 타입은 객체, 그 객체 안의
age
는number
여야 한다! 라고 명시하고 있는 것이다.
이를 인터페이스를 이용해 나타내면 다음과 같이 바뀐다.
interface personAge {
age: number;
}
function logAge(obj: personAge) {
console.log(obj.age);
}
let person = { name: 'Capt', age: 28 };
logAge(person);
어떤가? 먼저의 코드보다 좀 더 명시적이지 않은가??
파라미터의 세부적인 타입에 대한 명시까지 어렵지 않게 할 수 있다.
위의 예시를 보면 다음과 같은 추론이 가능하다.
다시 말해, 인터페이스에 정의된 속성, 타입의 조건만 만족한다면 객체의 속성 갯수가 더 많아도 무관하다는 것이다. 또한, 인터페이스에 정의된 속성의 순서를 고려하지 않아도 된다.
인터페이스를 사용할 때 인터페이스에 정의되어 있는 속성을 모두 다 꼭 사용하지 않아도 된다. 이를 옵션 속성이라고 한다. 다음 문법을 보자.
interface 인터페이스_이름 {
속성?: 타입;
}
이처럼 속성의 끝에 ?
를 추가하는 간단한 작업만으로 속성의 불가결성을 없앨 수 있다.
이에 대한 예시를 살펴보자.
interface CraftBeer {
name: string;
hop?: number;
}
let myBeer = {
name: 'Saporo'
};
function brewBeer(beer: CraftBeer) {
console.log(beer.name); // Saporo
}
brewBeer(myBeer);
코드를 살펴보면 brewBeer()
에서 Beer
인터페이스를 인자의 타입으로 선언했음에도 불구하고, 인자로 넘긴 객체에는 hop
속성이 존재하지 않는다. 이는 hop
을 옵션 속성으로 선언했기 때문이다.
옵션 속성의 장점은 단순히 인터페이스를 사용할 때 속성을 선택적으로 적용할 수 있다는 것 뿐만 아니라 인터페이스에 정의되어 있지 않은 속성에 대해서 인지시켜줄 수 있다는 점이다.
다음 예제를 살펴보자.
interface CraftBeer {
name: string;
hop?: number;
}
let myBeer = {
name: 'Saporo'
};
function brewBeer(beer: CraftBeer) {
console.log(beer.brewery); // Error: Property 'brewery' does not exist on type 'Beer'
}
brewBeer(myBeer);
위의 코드처럼 인터페이스에 정의되어 있지 않은 속성에 대해서 오류를 표시한다. 만일 아래와 같이 오탈자가 났었다면 그것 역시 알려주었을 것이다.
interface CraftBeer {
name: string;
hop?: number;
}
function brewBeer(beer: CraftBeer) {
console.log(beer.nam); // Error: Property 'nam' does not exist on type 'Beer'
}
저 위에서 추론한 내용과 함께 생각을 종합해보면 다음과 같다.
정의된 인터페이스의 속성만 만족시킨다면 이보다 속성이 많은 객체가 파라미터로 들어올 수 있다. 그러나 인터페이스로 정의된 속성만 사용이 가능해진다!
읽기 전용 속성은 인터페이스로 객체를 처음 생성할 때만 값을 할당하고 그 이후에는 변경할 수 없도록 하는 속성을 의미한다. 문법은 다음과 같이 readonly
를 앞에 붙인다.
interface CraftBeer {
readonly brand: string;
}
인터페이스로 객체를 선언하고 나서 수정하려고 하면 다음과 같은 오류가 발생한다.
let myBeer: CraftBeer = {
brand: 'Belgian Monk'
};
myBeer.brand = 'Korean Carpenter'; // error!
배열을 선언할 때 ReadonlyArray<T>
타입을 사용하면 읽기 전용 배열을 생성할 수 있다.
let arr: ReadonlyArray<number> = [1,2,3];
arr.splice(0,1); // error
arr.push(4); // error
arr[0] = 100; // error
위처럼 배열을 ReadonlyArray
로 선언하면 배열의 내용을 변경할 수 없다. 선언하는 시점에만 값을 정할 수 있으니 주의해서 사용해야 한다.
타입스크립트는 인터페이스를 이용해 객체를 선언할 때 엄밀한 속성 검사를 진행한다.
다음 예시를 살펴보자.
interface CraftBeer {
brand?: string;
}
function brewBeer(beer: CraftBeer) {
// ..
}
brewBeer({ brandon: 'what' }); // error: Object literal may only specify known properties, but 'brandon' does not exist in type 'CraftBeer'. Did you mean to write 'brand'?
위 코드를 보면 CraftBeer
인터페이스에는 brand
라고 선언되어 있지만 brewBeer()
함수에 인자로 넘기는 myBeer
객체에는 brandon
이 선언되어 있어 오탈자 점검을 요하는 오류가 난다.
만일 이런 타입 추론을 무시하고 싶다면 아래와 같이 선언해야 한다.
let myBeer = { brandon: 'what' }';
brewBeer(myBeer as CraftBeer);
궁금한 점. 만일 이렇게 코드가 짜여지면 brewBeer 함수의 인자로는 무엇이 전달되는 건지...?
{brandon: 'what'}
이 전해져도 아무 쓸모가 없는 것이 아닌가?
그럼에도 불구하고 만일 인터페이스를 정의하지 않은 속성들을 추가로 사용하고 싶을 때는 아래와 같은 방법을 사용한다.
interface CraftBeer {
brand?: string;
[propName: string]: any;
}
인터페이스는 함수의 타입을 정의할 때에도 사용할 수 있다.
interface login {
(username: string, password: string): boolean;
}
이렇게 함수의 인자와 반환값의 타입을 명시한다.
let loginUser: login;
loginUser = function(id: string, pw: string) {
console.log('로그인 했습니다');
return true;
}
C#이나 자바처럼 타입스크립트에서도 클래스가 일정 조건을 만족하도록 타입 규칙을 정할 수 있다.
class의 extends 대신 implements가 사용된다.
interface CraftBeer {
beerName: string;
nameBeer(beer: string): void; // 함수의 타입 지정
}
class myBeer implements CraftBeer {
beerName: string = 'Baby Guinness';
nameBeer(b: string) {
this.beerName = b;
}
constructor() {}
}
클래스와 마찬가지로 인터페이스도 인터페이스 간 확장이 가능하다.
interface Person {
name: string;
}
interface Developer extends Person {
skill: string;
}
let fe = {} as Developer;
fe.name = 'josh';
fe.skill = 'TypeScript';
물론 여러 인터페이스를 상속받아 사용할 수도 있다.
interface Person {
name: string;
}
interface Drinker {
drink: string;
}
interface Developer extends Drinker, Person {
skill: string;
}
let fe = {} as Developer;
fe.name = 'josh';
fe.skill = 'TypeScript';
fe.drink = 'Beer';
자바스크립트의 유연하고 동적인 타입 특성에 따라 인터페이스 역시 여러 타입을 조합해 만들 수 있다.
다음 예시와 같이 함수 타입이면서 객체 타입을 정의할 수 있는 인터페이스가 있다.
interface CraftBeer {
(beer: string): string;
brand: string;
brew(): void;
}
function myBeer(): CraftBeer {
let my = (function(beer: string) {}) as CraftBeer;
my.brand = 'Beer Kitchen';
my.brew = function() {};
return my;
}
let brewedBeer = myBeer();
brewedBeer('My First Beer');
brewedBeer.brand = 'Pangyo Craft';
brewedBeer.brew();
와우...함수이면서 객체일 수 있다...!!
클래스를 확장해서 만들어진 인터페이스도 존재한다. 그러나 이러한 인터페이스 중 부모로 사용된 클래스의 속성이 private
이거나 protected
인 경우, 해당 인터페이스는 부모 클래스 혹은 부모를 통해 생성된 타 확장 클래스만 구현해낼 수 있다.
다음 코드를 살펴보자.
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() {}
}
class TextBox extends Control {
select() {}
}
class ImageControl implements SelectableControl {
//Class 'ImageControl' incorrectly implements interface 'SelectableControl'.Types have separate declarations of a private property 'state'.
private state: any;
select() {}
}
SelectableControl
은 private
속성을 가진 클래스 Control
의 확장이다. 이러한 경우 SelectableControl
은 위의 Button
과 같이 Control
을 확장해 만든 클래스에만 implements
할 수 있다. 아래의 ImageControl
은 Control
의 영향을 받은 것이 아무 것도 없기 때문에 SelectableControl
이 implements
될 수 없는 것이다.
솔직히 조금 어렵다...번역본이 없어 원본 문서를 일단 이해하려고 했는데 어디에 이걸 유용하게 써야할지 감이 잘 안 온다...이 글을 읽고 혹시라도 아시는 분은 알려주시면 감사하겠습니다:)
인터페이스에 대한 기본적인 정리가 마무리되었다. 다음 시간에는 이넘에 대해 다뤄보자.