type과 interface 차이점

nearworld·2023년 1월 21일
0

typescript

목록 보기
15/28
post-thumbnail

typeinterface의 차이점을 살펴보는 게 주 목적이지만 어떤 부분이 같은 것인지도 확인해보려고 한다

공통점

1. 타입 에러

type PointType = {
  x: number,
  y: number
}

interface PointInterface {
  x: number,
  y: number
}

type alias로 작성된 PointType과 인터페이스 키워드로 작성된 PointInterface가 있다.

const a = (point: PointType) => void;
const b = (point: PointInterface) => void;

위 코드에서는 두 함수가 보이고 각 함수의 매개변수 타입은 PointType, PointInterface 이다.

a({10, '20'});
b({10, '20';});

a, b 함수 모두 호출단계에서 잘못된 타입을 인자로 받고 있으므로 타입 에러가 발생할 것이다.

(property) y: number
Type 'string' is not assignable to type 'number'.(2322)
input.tsx(3, 3): The expected type comes from property 'y' which is declared here on type 'PointType'
Type 'string' is not assignable to type 'number'.(2322)
input.tsx(8, 3): The expected type comes from property 'y' which is declared here on type 'PointInterface'

interfacetype alias는 같은 타입 에러를 일으키고 있다.

2. 타입스크립트에서 extends, implements

2-1. extends

type Point = {
  x: number,
  y: number
}
interface ThreeDimensions extends Point {
  z: number
}

interfacetype alias로 지정된 타입을 상속할 수 있다.

2-2. implements

class Rectangle implements Point {
  x = 1;
  y = 1;
}

심지어 클래스도 타입 Pointimplement하여 특정 필드들을 강제 사용하도록 할 수 있다.
Rectangle클래스의 퍼블릭 필드 x, y의 타입은 클래스 Rectangleimplements하고 있는 Point 타입에 의해 강제 지정된 상태다. 만약, x, y 필드들을 사용하지 않는다면 타입 에러가 발생하게 된다.

implements
implements는 객체지향에서 사용되는 용어로 클래스가 인터페이스를 재정의할때 사용한다.
타입스크립트에서는 implements 키워드를 이용해 클래스의 특정 필드를 정의해야만 클래스 사용이 가능하도록 강제할 수 있다.

type Point = {
  x: number,
  y: number,
  z: number
}

interface ThreeDimensions extends Point {
  z: number
}

class Rectangle implements ThreeDimensions {
  x = 1;
  y = 1;
  z = 1;
}

인터페이스 ThreeDimensions를 구현하고있는 Rectangle 클래스.

type Shape = {
  area(): number
}

interface Perimeter {
  perimeter(): number
}

class Rectangle implements Shape, Perimeter, Point {
  x = 10;
  y = 20;
  area() {
    return this.x * this.y;
  }
  perimeter() {
    return 2 * (this.x + this.y);
  }
}

위 코드에서 클래스 Rectangletype aliasinterface 타입들을 implements하고 있다. implements하고 있는 타입들을 클래스에서 구현하고 있지 않다면 에러가 발생하게 되므로 implements를 사용한 타입들은 무조건 클래스에서 구현해줘야한다!

이렇게 클래스에서 타입을 implements 하는 경우를 클래스 제약조건 Class Constraint라고 부르는 블로그 글도 확인했다.

class Rectangle implements Shape, Perimeter, Point {

Shape, Perimeter, Point 타입, 인터페이스들을 위처럼 나열하는 방식으로 코드를 작성할 수도 있지만 묶어서 정리하여 코드의 의미를 더 명확히 할 수도 있다.

type RectangleShape = Shape & Perimeter & Point

class Rectangle implements RectangleShape {
  x = 10;
  y = 10;
  area() {
   return this.x * this.y; 
  }
  perimeter() {
    return 2 * (this.x + this.y);
  }
}

타입스크립트의 유틸리티 타입인 Partial를 이용하여 한가지 메소드만 구현해도 클래스를 만들 수 있도록 허용해보자.

type RectangleShape = Partial<Shape | Perimeter> & Point;

class Rectangle implements RectangleShape {
  x = 10;
  y = 10;
  perimeter() {
    return 2 * (this.x + this.y);
  }
}

Partial<Shape | Perimeter> 덕분에 perimeter 메서드만 구현해도 된다.

type Shape = {
  area(): number
}
type Perimeter = {
  perimeter(): number
}
type RectangleShape = (Shape | Perimeter) & rect;
class Rectangle implements RectangleShape {
  
}

클래스 RectangleRectangleShape 타입을 구현하고 있다. 하지만 위 코드는 아래의 에러를 일으킨다.

하나의 클래스는 하나의 오브젝트 타입이나 교차 타입만 구현할 수 있다.
그 이유는 클래스 정의는 한 번만 하기때문에 다른 타입일 가능성이 있을리 없기 때문이다.

type RectangleShape = (Shape | Perimeter) & rect;
// 유니온 타입을 감싸고 있는 괄호는 없어도 무방하다.

에러의 원인이 되는 부분은 바로 RectangleShape 타입에 유니온 타입이 있기 때문이다.
(Shape | Perimeter) 만약 이 부분이 교차 타입인 Shape & Perimeter & Point였다면 클래스 Rectangle은 모두 다 구현해야하는 하나의 구현 가능성만 가지기 때문에 정상적으로 RectangleShape 타입을 구현할 수 있다.

type RectangleShape = (Shape | Perimeter) & Point;
const rectangle: RectangleShape = {
  x: 1,
  y: 2,
  area() {
    return x * y;
  }
}

일반 객체의 경우에는 객체에 등록될 메서드가 여러 종류로 바뀔 가능성이 있기 때문에 유니온 타입이 허용된다. (클래스는 한 번 선언 후 다시는 재정의될 일이 없기때문에 유니온 타입이 허용되지 않는 다는 점을 기억해두자!)

위 코드를 만지다가 요상한 부분을 발견했는데

type Point = {
  x: number,
  y: number,
  z: number
}
type Shape = {
  area(): number
}
type Perimeter = {
  perimeter(): number
}
type RectangleShape = (Shape | Perimeter) & rect;

const rectangle: RectangleShape = {
  x: 1,
  y: 2,
  area() {
    return x * y;
  },
  perimeter() {
    return 2 * (x + y);
  }
}

위 코드에서 ShapePerimeter 타입 두 개 모두를 객체 내에서 사용하게 되면 그 메서드들을 사용할 수 없게 된다. 에러는 일어나지 않지만 메서드가 없는 것으로 나온다.

차이점

간단한 차이점

타입 별칭(type alias)은 타입 선언에 모든 데이터 타입을 지정할 수 있다.

interface는 오직 객체 타입에만 타입을 지정할 수 있다.

type alias에 교차 타입 연산자(&)를 사용하여 다른 타입으로 확장

type ShapeOrPerimeter = Shape | Perimeter;
type RectangleShape = {

} & ShapeOrPerimeter & Point;

위 코드에서 ShapeOrPerimeter은 유니온 타입이고 RectangleShapeShapeOrPerimeter 타입으로 확장하고 있다.
type alias 경우에는 다른 타입이 유니온 타입이더라도 확장할 수 있도록 허용하고 있기에 아무 에러없이 사용할 수 있는 코드이다.

interface에 extends 키워드를 사용하여 다른 타입으로 확장

type ShapeOrPerimeter = Shape | Perimeter;
interface RectangleShape extends ShapeOrPerimeter & Point {
  
}

위 코드는 에러를 일으키게 된다.

아까 위에서 클래스가 유니온 타입을 구현하려고 할때 발생했던 에러와 동일한 에러다.
클래스의 경우에는 클래스는 한 번만 선언되고 그 클래스를 다시 정의할 일이 없기에 유니온 타입이 허용되지 않는다고 했다.
인터페이스의 경우에도 마찬가지로 인터페이스는 한 번만 선언해두고 그 선언된 인터페이스를 토대로 타입으로 사용하는 정적인 청사진 기능을 하기 때문에 유니온 타입으로의 확장이 허용되지 않는다.

3. 인터페이스 병합 (Declaration Merging)

들어가기에 앞서, 위 용어 Declaration Merging은 인터페이스에 관련된 용어임을 미리 알아두자. 한국말로는 인터페이스 병합이라고 한다.

interface Box {
  height: number
}
interface Box {
  width: number
}
interface Box {
  scale: number
}

const box: Box = {height: 10, width: 10, scale: 1};

인터페이스 Box가 밑으로 중복해서 선언되고 있다. 중복 선언으로 에러가 발생할 것이라는 생각을 할 수도 있지만 타입스크립트는 같은 이름의 인터페이스가 중복 선언되는 경우에는 Declaration Merging 이라고 해서 선언될때마다 선언된 인터페이스 명세가 이어붙도록 작동한다.

type Box {
  height: number
}
type Box {
  width: number
}
type Box {
  scale: number
}

하지만 type alias를 이용한 타입 선언의 경우에는 중복이라고 판정받고 타입 에러가 발생한다. 타입스크립트에서 타입은 오로지 1개만 선언될 수 있는 유니크한 특성을 지니기 때문에 중복 선언이 불가능하다.

그럼 인터페이스 병합은 굳이.. 중복 선언해가며 인터페이스를 만들어야하는 이유가 뭐지 라는 의문점이 든다. 인터페이스 병합은 아래와 같은 상황에서 사용 가능하다.

// 라이브러리 파일 내의 인터페이스
export interface Theme {
  dark: 
}

import {Theme} from '라이브러리'
interface Theme {
  light: '라이트 모드'
}

라이브러리 개발자가 만들어둔 Theme 인터페이스를 사용할때 인터페이스 병합을 사용하여 내가 작성중인 코드에 타입 프로퍼티를 추가하는 식으로 타입을 커스터마이징해 볼 수 있다.

결론

위에서 타입과 인터페이스가 클래스와 어떻게 상호작용하는지 알아보았다.
중요하게 볼 부분은 유니온 타입이라고 생각이 들었다.

그리고 리액트에서 Props의 타입을 type alias로 지정할지 아니면 interface로 지정할지 헷갈리던 것을 좀 명확하게 구별하게 해주는 기준점이 하나 생겼다.

interface X {
  x: number,
}
type Y = {
  y: number
}
type Props = { z: number } & (X | Y);

Propstype alias로 선언된 경우에는 유니온 타입으로 확장해도 문제가 없다.

interface X {
  x: number
}
type Y = {
  y: number
}
interface Props extends (X | Y) {
  z: number
}

하지만 Propsinterface로 선언된 경우에는 유니온 타입으로 확장되는게 불가능하다.
아무래도 Props의 유연성이 낮아지게 된다고 볼 수 있겠다.

profile
깃허브: https://github.com/nearworld

0개의 댓글