Typescript 개념잡기(3)

오형근·2022년 5월 6일
0

Typescript

목록 보기
3/15
post-thumbnail

저번 글에 이어 인터페이스에 대한 내용을 다뤄보자.

타입스크립트 핸드북(타입스크립트 핸드북)을 보고 공부한 것을 정리한 글입니다.
기본적인 JS 지식은 가진 상태라고 가정합니다.


인터페이스

핸드북에서는 인터페이스를 다음과 같이 정의했다.

인터페이스는 상호 간에 정의한 약속 혹은 규칙을 의미한다. 타입스크립트에서의 인터페이스는 보통 다음과 같은 범주에 대해 약속을 정의할 수 있다.

그에 따른 약속은 다음과 같다.

  • 객체의 스펙(속성과 속성의 타입)
  • 함수의 파라미터
  • 함수의 스펙(파라미터, 반환 타입 등)
  • 배열과 객체를 접근하는 방식
  • 클래스

이게 무슨 말인가 하면, 위의 다섯 범주가 어떤 타입의 값들을 가지는지 명시해주는 도구가 바로 인터페이스인 것이다.

인터페이스 맛보기

다음 예제를 살펴보자.

let person = { name: 'Capt', age: 28 };

function logAge(obj: { age: number }) {
  console.log(obj.age); // 28
}
logAge(person); // 28

logAge()함수에서 받는 인자의 형태는 age를 속성으로 갖는 객체이다. 이처럼 인자를 받을 때 단순 타입 뿐 아니라 객체의 속성 타입까지 정의할 수 있다.

즉, 파라미터의 타입은 객체, 그 객체 안의 agenumber여야 한다! 라고 명시하고 있는 것이다.

이를 인터페이스를 이용해 나타내면 다음과 같이 바뀐다.

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() {}
}

SelectableControlprivate 속성을 가진 클래스 Control의 확장이다. 이러한 경우 SelectableControl은 위의 Button과 같이 Control을 확장해 만든 클래스에만 implements할 수 있다. 아래의 ImageControlControl의 영향을 받은 것이 아무 것도 없기 때문에 SelectableControlimplements될 수 없는 것이다.

솔직히 조금 어렵다...번역본이 없어 원본 문서를 일단 이해하려고 했는데 어디에 이걸 유용하게 써야할지 감이 잘 안 온다...이 글을 읽고 혹시라도 아시는 분은 알려주시면 감사하겠습니다:)


인터페이스에 대한 기본적인 정리가 마무리되었다. 다음 시간에는 이넘에 대해 다뤄보자.

0개의 댓글