타입스크립트6: Interface

윤뿔소·2023년 2월 17일
0

TS

목록 보기
4/5

type은 정말 유용한 존재였다.

  1. 특정 값이나 객체의 값에 대한 타입을 지정해줄 수 있다.
  2. Type alias(타입에 대한 별명)를 만들어줄 수 있다.
  3. 타입을 특정한 값을 가지도록 제한할 수 있다.
// 1, 2
type Health = number;
// 3
type Team = "red" | "blue" | "yellow";

type Player = { nickname: string; team: Team; health: Health };

const rhino: Player = {
  nickname: "rhino",
  team: "yellow",
  health: 1,
};

이런 식으로 말이다! type 키워드는 활용할 것이 많다.

그렇다면 type Player = { ... };부분에 type 지우고 interface을 넣은다음 =도 지운다면? 이렇게 된다.

interface Player {
  nickname: string;
  team: Team;
  health: Health;
}

바로 여기서 interface가 나온다.

Interface

오로지 오브젝트 모양을 타입스크립트에게 설명해 주기 위한 키워드

즉, type보다 활용도는 떨어지지만 간단하게 나타낼 때 좋다.

type Player = {
  nickname: string;
  team: Team;
  health: Health;
}

interface Player {
  nickname: string;
  team: Team;
  health: Health;
}

위 두개는 같다. 진짜 객체의 타입 설정에 유용하게 쓰일 수 있는 것.

인터페이스의 또 다른 특징으로는 속성(Property)들을 ‘축적’시킬 수 있다는 것이다.

interface User {
  name: string;
}
interface User {
  lastName: string;
}
interface User {
  health: number;
}
const rhino: User = { name: "rhino", lastName: "n", health: 10 };

Type과 차이점

참고: 공식문서

1. 타입을 특정한 값이나 alias 자체를 interface가 설정하지 못한다.

type Team = "red" | "blue" | "yellow";
type Health = number;

// X
interface Team = "red" | "blue" | "yellow";
interface Hello = string;

2. 상속받는 문법 및 중복 사용

  1. type
type UserA = {
  firstName: string;
};
type UserAA = UserA & {
  lastName: string;
};
// 이건 불가능, 이미 선언되어서 중복이 안됨
type UserAA = {
  health: number;
};
  1. interface
interface UserB {
  firstName: string;
}
interface UserBB extends UserB {
  lastName: string;
}
// 가능! UserBB에 쌓였음
interface UserBB {
  health: number;
}
// 심지어 이렇게도 가능
interface UserB {
  lastName: string;
  health: number;
}

이런 활용도의 차이가 있다. 그냥 적기만 하면 합쳐주니 확장성을 고려한다면 인터페이스가 더 낫다.

3. 모양을 잡는 용도

type User = {
  firstName: string;
  lastName: string;
  sayHi(name: string): string;
  fullName(): string;
};

interface User {
  firstName: string;
  lastName: string;
  sayHi(name: string): string;
  fullName(): string;
}

이런 식으로 쓸 수 있다.

그렇다면 굳이 Interface를 쓰는 이유는 무엇일까?

쓰는 이유

1. 직관적이다.

객체 지향과 비슷한 문법을 사용해 extends로 상속받을 수 있기도 하고 익숙하기 때문이다.

interface User {
  name: string;
}
interface Player extends User {}

const rhino: Player = {
  name: "rhino",
};

이렇게 상속도 받고이런 식으로 읽기 전용도 만들 수 있어 클래스형 컴포넌트랑 비슷하다.

물론 &를 이용해서 type 키워드 간에도 상속을 받을 수 있긴 하다.

2. 기능 분리

type은 좀 더 다재다능하고 유연해서 아무데서 쓰여도 되지만 Interface는 객체지향의 디자인을 따와 객체의 설명해주기 때문에 읽기도 편하고 type과는 분리되어 그 객체의 타입과 내용에 더 집중할 수 있다. 다시 말해 특정 모양을 따르도록 만든 틀이라면 interface를 쓰는 것이 여러모로 편리하고 좋다는 얘기.

3. 확장성

위의 차이에서 보았듯이 인터페이스는 중복을 허가하니 나중에 타입의 수정이나 병합을 가져온다면 최대한 인터페이스를 쓰는 것이 추후의 일에 더 좋을 것이다.

Interface와 추상화

저번에 배웠던 추상 클래스도 Interface와 접목시킬 수 있다.

abstract class User {
  constructor (
    protected firstName:string, 
    protected lastName:string
  ) {}
  abstract sayHi (name:string):string 
  abstract fullName():string
}

여기 추상클래스와 추상화를 가진 메소드 2개가 있다. 저번에서 배운 것처럼 User를 상속받으려면 청사진만 있는 sayHi, fullName 메소드를 무조건 구현해야하고, firstName lastName을 쓸 수 있다.

이제 구현해보자

abstract class User {
  constructor(
    protected firstName: string, 
    protected lastName: string
  ) {}
  abstract sayHi(name: string): string;
  abstract fullName(): string;
}
class Player extends User {
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
  sayHi(name: string) {
    return `Hello ${name}. My name is ${this.fullName()}`;
  }
}

이렇게 만들어서 구현할 수 있다. 추상 클래스 특징으로 User의 인스턴스, 즉 직접적으로 구현할 수 없고, Player같은 상속 받은 클래스가 수많이 나오고, 수많은 인스턴스들이 나올 것이다.

저번에 배웠듯이 User는 JS로 변환될 것이다.

이런 식으로 변환되는 걸 보니 의문이 생긴다. User를 직접적으로 만지지도 않을 거고, 상속받고 파생되는 클래스와 인스턴스로 쓸 건데 JS로 컴파일 되도 남아있나? 없앨 수 있는 방법이 있을까?

바로 이때 Interface를 쓰는 것이다!! 인터페이스는 JS로 컴파일되면 없어지고, 클래스보다 훨씬 가벼우면서도 교체가 가능한 점이 있어 이를 이용하면 파일 사이즈도 줄어들고 좋다.

추상클래스를 인터페이스로 교체하기

implements라는 키워드가 핵심이다.

interface User {
  firstName: string;
  lastName: string;
  sayHi(name: string): string;
  fullName(): string;
}
class Player implements User {
  constructor(
    public firstName: string, 
    public lastName: string
  ) { }
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
  sayHi(name: string) {
    return `Hello ${name}. My name is ${this.fullName()}`;
  }
}
  1. 추상클래스는 interface로 바뀜
  2. implements 키워드를 써서 User를 구현해야하는(추상클래스 상속) Player 클래스 생성
  3. 추상클래스에 썼던 생성자 작성
    + 여기에 차이점이 있는데 보호레벨을 public으로 받았다. 인터페이스를 상속받을 땐 다른 레벨이 아닌 public로만 받아야한다.
  4. 구현한 메소드도 그대로 작성

그러면 결과물이 이렇게 나온다. 아까완 달리 JS로 Player만 나오는 걸 확인할 수 있다. 이러면 클래스가 하나 적어지는 등의 코드양이 줄어드니 좋다!

또 2개 이상의 인터페이스를 상속받을 수 있다.

interface User {
  ...
}
interface Human {
  health: number;
}
class Player implements User, Human {
  constructor(
  	public firstName: string, 
  	public lastName: string, 
    public health: number
  ) {}
  
  ...
}

이런 식으로 받을 수 있다. 되게 유용하다. 나중에 패턴의 병합이나 수정이 들어온다면 이렇게 상속받으면 된다.

참고: 함수형은?

함수형은 더 쉽다. 직관적이고 사실 리액트에 쓴다면 함수형에 많이들 쓸 것이다. 나는 이렇게 많이 썼다.

// 파라미터에 User 적용
function makeUser(user: User) {
  return "Scuccess!";
}
// return까지 User 타입으로
function makeUserReturn(user: User): User {
  return { firstName: "nico", lastName: "las", fullName: () => "Xx", sayHi: (name) => "string" };
}
makeUser({ firstName: "nico", lastName: "las", fullName: () => "Xx", sayHi: (name) => "string" });

위에서 봤던 것처럼 추상화가 적용되어 파라미터나 리턴 값에 User의 모양과 다르다면 오류가 뜰 것이다. 추상 클래스와 비슷하게 모양을 정하는 것이 적용된다는 것! 이렇게 연결되는 것이다.

profile
코뿔소처럼 저돌적으로

5개의 댓글

comment-user-thumbnail
2023년 2월 28일

타입스크립트 상세하게 적어주셔서 감사합니다 예습하고가요 !!

답글 달기
comment-user-thumbnail
2023년 3월 3일

클래스 인터페이스 전환 부분에서 예시와 함께 설명해주시는 부분 인상적입니다. 덕분에 이해 되었어요!

답글 달기
comment-user-thumbnail
2023년 3월 4일

저도 다음 주부터 타입스크립트를...

답글 달기
comment-user-thumbnail
2023년 3월 4일

축적도 가능했었군요 ... !!

답글 달기
comment-user-thumbnail
2023년 3월 5일

요즘 타입스크립트 강의를 듣고 있는데, 이걸 보니 얼른 진도를 뺴야겠다는 생각뿐이네요 ㅎㅎ 모르는거 여쭤봐도 되나용

답글 달기