ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 오답노트 작성. type과 interface.
    👩🏻‍💻 정리/TypeScript 2024. 1. 10. 17:27

    이전에 작성해 놓은 [TypeScript] 인터페이스(Interface)에서 '작성 중'으로만 적어놓고 넘어갔던 부분에 대한 질문을 받았다. 역시나 이래놓고 작성을 추가로 하지 않았기 때문에 말문이 막혔는데...// (OTL)

    앞으로는 포스팅에 '작성 중' 표시는 웬만해서 하지 말자. 무조건 작성이 다 된 것만 업로드하도록 하자.

    앞으로는 이런 일이 없게 이전에 작성한 포스팅을 검토하여 '작성 중'으로 되어 있는 부분을 수정하도록 하자. 그리하여 오랜만에 돌아온 TIL은 오답노트, type vs interface!


    타입 별칭(type alias)처럼 인터페이스(interface) 역시 타입에 이름을 지어주는 수단이다. 타입 별칭과 인터페이스는 문법만 다를 뿐, 같은 기능을 수행한다. 둘 다 형태(shape)를 정의하며 두 형태는 서로 할당할 수 있다.

    타입 별칭은 type이라는 키워드를 사용하여 타입에 이름을 붙여서 사용할 수도 있다. 타입을 재사용하거나, 객체를 위한 타입을 정의할 때 많이 사용한다.

    인터페이스는 interface라는 키워드를 사용하여 타입 별칭과 동일하게 객체의 타입을 정의할 수 있다.

    // 타입 별칭
    type Sushi = {
        calories: number;
        salty: boolean;
        tasty: boolean;
    }
    // 인터페이스
    interface Sushi {
        calories: number;
        salty: boolean;
        tasty: boolean;
    }

    타입 별칭의 조합은 아래와 같이 인터섹션(&)을 활용하여 확장 및 조합할 수 있다. Food라는 공통 정보를 정의한 타입을 별도로 두고, 각 음식들은 Food 타입을 인터섹션으로 조합하면서 재정의한다.

    // 타입 별칭의 확장(&)
    type Food = {
        calories: number;
        tasty: boolean;
    }
    type Sushi = Food & {
        salty: boolean;
    }
    type Cake = Food & {
        sweet: boolean;
    }

    위 코드를 인터페이스로 아래와 같이 정의할 수도 있다. 인터섹션(&)으로 확장했던 타입 별칭과 다르게 extends 키워드로 해당 인터페이스를 상속하여 확장한다.

    // 인터페이스의 확장(extends)
    interface Food {
        calories: number;
        tasty: boolean;
    }
    interface Sushi extends Food {
        salty: boolean;
    }
    interface Cake extends Food {
        sweet: boolean;
    }

    인터페이스 역시 인터섹션(&)을 활용하여 기존에 정의했던 인터페이스를 합쳐 새로운 타입을 만들 수 있다.

    // 교차 타입 확장
    interface Food {
        calories: number;
        tasty: boolean;
    }
    
    interface Salty {
        salty: boolean;
    }
    
    type Sushi = Food & Salty;

    그렇다면 이 둘의 차이점은 무엇일까?

    첫째, 타입 별칭(type)은 더 일반적이어서 타입 별칭의 오른편에는 타입 표현식(타입, &, | 등의 타입 연산자)을 포함한 모든 타입이 등장할 수 있다. 반면, 인터페이스(interface)의 오른편에는 반드시 형태가 나와야 한다. 예를 들어 아래와 같은 타입 별칭 코드는 인터페이스로 재작성이 불가하다.

    type A = number;
    type B = A | string;

    둘째, 인터페이스를 상속할 때 타입스크립트는 상속받는 인터페이스의 타입에 상위 인터페이스를 할당할 수 있는지를 확인한다. 이게 무슨 소리냐면.. 아래 코드를 살펴보자.

    interface A {
        good(x: number): string;
        bad(x: number): string;
    }
    interface B extends A {
        good(x: string | number): string;
        bad(x: string): string; // 에러 발생!
    }

    A라는 인터페이스를 B에 상속하려고 할 때, '인터페이스 B는 인터페이스 A를 올바르게 상속받을 수 없다'는 에러가 발생한다. 이유는 바로 타입이 호환되지 않기 때문이다.

    올바르지 않은 상속을 하는 경우 발생하는 에러

    하지만 만약, extends가 아닌 인터섹션(&)으로 바꾸면 타입스크립트는 확장하는 타입을 최대한 조합하는 방향으로 동작한다. 결과적으로 컴파일 에러가 발생하지 않고, bad를 오버로드한 시그니처가 만들어진다.

    type A = {
        good(x: number): string;
        bad(x: number): string;
    };
    type B = A & {
        good(x: string | number): string;
        bad(x: string): string; // 에러 없이 오버로드됨!
    };

    때문에, 객체 타입의 상속을 표현할 때 인터페이스에 제공되는 타입스크립트의 할당성 확인 기능을 이용하면 쉽게 에러를 검출할 수 있다.

    셋째, 이름과 범위가 같은 인터페이스가 여러 개 있다면 이들이 자동으로 합쳐진다. 이를 '선언 합침(declaration merging)'이라 부른다. 반면, 같은 조건에서 타입 별칭이 여러 개라면 컴파일 타임 에러가 난다.


    그래서 그 둘은 언제언제 구분해서 사용하는 게 좋을까? 아래와 같은 기준으로 구분해서 사용하려고 한다.

    1) 인터페이스 사용: 선언 합침이 필요하거나 객체 및 클래스의 구조를 정의하려는 경우
    2) 타입 별칭 사용: 유니온 타입과 교차 타입에 더 많은 유연성이 필요하거나 리터럴 유형으로 작업해야 하는 경우

    추가적으로, 타입스크립트 공식 문서에는 타입 별칭보다 인터페이스를 사용하라고 되어 있다.

    감히 추측해보건대, 인터페이스는 일반적으로 클래스 인스턴스의 모양을 정의하는 데 사용되고, 타입 시스템과 객체 지향 프로그래밍 개념 간의 긴밀한 관계를 조성하기 때문에 클래스와의 호환성 측면에서 인터페이스를 사용하라고 권장하는 거 같다. 웬만한 경우에서 인터페이스를 사용하는 것이 더 좋을 거 같다고 판단된다!

    💡 앞으로 나는 객체는 무조건 interface, 한줄로 끝날 수 있는 타입의 경우 type 별칭을 사용해서 작성을 하려고 한다!


    참고 서적: <타입스크립트 프로그래밍> - 보리스 체르니