type
과 interface
의 차이점을 살펴보는 게 주 목적이지만 어떤 부분이 같은 것인지도 확인해보려고 한다
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'
interface
와 type alias
는 같은 타입 에러를 일으키고 있다.
type Point = {
x: number,
y: number
}
interface ThreeDimensions extends Point {
z: number
}
interface
는 type alias
로 지정된 타입을 상속할 수 있다.
class Rectangle implements Point {
x = 1;
y = 1;
}
심지어 클래스도 타입 Point
를 implement
하여 특정 필드들을 강제 사용하도록 할 수 있다.
Rectangle
클래스의 퍼블릭 필드 x
, y
의 타입은 클래스 Rectangle
이 implements
하고 있는 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);
}
}
위 코드에서 클래스 Rectangle
은 type alias
와 interface
타입들을 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 {
}
클래스 Rectangle
이 RectangleShape
타입을 구현하고 있다. 하지만 위 코드는 아래의 에러를 일으킨다.
하나의 클래스는 하나의 오브젝트 타입이나 교차 타입만 구현할 수 있다.
그 이유는 클래스 정의는 한 번만 하기때문에 다른 타입일 가능성이 있을리 없기 때문이다.
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);
}
}
위 코드에서 Shape
과 Perimeter
타입 두 개 모두를 객체 내에서 사용하게 되면 그 메서드들을 사용할 수 없게 된다. 에러는 일어나지 않지만 메서드가 없는 것으로 나온다.
간단한 차이점
타입 별칭(type alias)은 타입 선언에 모든 데이터 타입을 지정할 수 있다.
interface는 오직 객체 타입에만 타입을 지정할 수 있다.
type ShapeOrPerimeter = Shape | Perimeter;
type RectangleShape = {
} & ShapeOrPerimeter & Point;
위 코드에서 ShapeOrPerimeter
은 유니온 타입이고 RectangleShape
이 ShapeOrPerimeter
타입으로 확장하고 있다.
type alias
경우에는 다른 타입이 유니온 타입이더라도 확장할 수 있도록 허용하고 있기에 아무 에러없이 사용할 수 있는 코드이다.
type ShapeOrPerimeter = Shape | Perimeter;
interface RectangleShape extends ShapeOrPerimeter & Point {
}
위 코드는 에러를 일으키게 된다.
아까 위에서 클래스가 유니온 타입을 구현하려고 할때 발생했던 에러와 동일한 에러다.
클래스의 경우에는 클래스는 한 번만 선언되고 그 클래스를 다시 정의할 일이 없기에 유니온 타입이 허용되지 않는다고 했다.
인터페이스의 경우에도 마찬가지로 인터페이스는 한 번만 선언해두고 그 선언된 인터페이스를 토대로 타입으로 사용하는 정적인 청사진 기능을 하기 때문에 유니온 타입으로의 확장이 허용되지 않는다.
들어가기에 앞서, 위 용어 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);
Props
가 type alias
로 선언된 경우에는 유니온 타입으로 확장해도 문제가 없다.
interface X {
x: number
}
type Y = {
y: number
}
interface Props extends (X | Y) {
z: number
}
하지만 Props
가 interface
로 선언된 경우에는 유니온 타입으로 확장되는게 불가능하다.
아무래도 Props
의 유연성이 낮아지게 된다고 볼 수 있겠다.