🌱 ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TypeScript] 타입(type)과 인터페이스(interface)의 차이점
    Front-End/TypeScript 2023. 12. 22. 23:45
    🍀 목차
    확장
    새 필드 추가
    사용 범위
    Index Signature
    type으로만 선언 가능한 타입들

     

     타입(type)과 인터페이스(interface)는 TypeScript에서 객체의 타입을 만드는 데 사용된다. type alias로는 객체 타입뿐 아니라 모든 유형의 타입을 만들 수 있는데, 이처럼 두 키워드의 특징과 차이점을 알아보자.

     

    확장

    type(교차 타입 사용)

    type Animal = {
        name: string   
        sound: string
    }
    
    type BearFeature = {
        honey: boolean
        sound: string
    }
    
    type Bear = Animal & BearFeature;
    
    const bear:Bear = {name:"Pooh",sound:"kkao~",honey:true};
    bear.name;
    bear.honey;
    bear.sound;

     

     

     타입은 &(intersection)을 활용하여 확장이 가능하다. 집합으로 생각해 보면 타입 Animal, 타입 BearFeature를 모두 만족하는 경우라고 생각하면 이해가 쉽다.

     

    intersection
    intersection

     

     

     교차 타입(intersection)을 사용하면 여러 타입을 합쳐 하나의 타입으로 사용할 수 있다. type Bear = Animal & BearFeature; Bear 타입의 모든 값은 Animal 타입의 값이면서 BearFeature 타입의 값이다.

     

    type Parent = {
        printName: (name: number) => void;
    }
    
    type Child = Parent & {
        // (name: number | string) => void 와 같아진다.
        printName: (name: string) => void;
    }
    
    const test: Child = {
        printName: (name: number | string) => {
            console.log('name: ' , name);
        },
    };
    
    test.printName(1); 
    test.printName('1');

     

     

     

    interface(extends 사용)

    interface Animal {
        name: string
    }
    
    interface Bear extends Animal {
        honey: boolean
    }
    
    const bear: Bear = {name:'fubao', honey:false};
    bear.name;
    bear.honey;

     

     인터페이스는 extends 키워드를 사용하여 확장이 가능하다. 

     

    interface Parent {
        printName: (name: number) => void;
    }
    
    // Error. Interface 'Child' incorrectly extends interface 'Parent'.
    interface Child extends Parent {
        printName: (name: string) => void;
    }
    
    // 그대신 아래와 같은 상속이 가능하다.
    
    interface Child extends Parent {
        printName: (name: string | number) => void;
    }
    
    // 라이브러리를 만들고 open API를 제공하면 사용자가 인터페이스의 해당 특징을 사용하여 확장할 수 있어 편하다.

     

     

     인터페이스들을 교차 타입으로 만들어 type alias로 선언하는 것도 된다.

     

    // 이런 것이 불가능
    interface Animal & BearFeature {
        name: string
        sound: string
    }
    
    // 이런 건 가능
    interface Animal {
        name: string   
        sound: string
    }
    
    interface BearFeature {
        honey: boolean
        sound: string
    }
    
    type Bear = Animal & BearFeature;

     


    type Foo = Bar & Baz & {
    	someProp: string;
    }
    
    interface Foo extends Bar, Baz {
    	someProp: string;
    }

     

     

    Interfaces create a single flat object type that detects property conflicts, which are usually important to resolve! Intersections on the other hand just recursively merge properties, and in some cases produce never.

    https://github.com/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections

     

     확장에 있어서 인터페이스는 중복으로 선언된 충돌을 해결하기 위해 단순 객체 타입을 생성한다. 인터페이스는 객체만 정의하기에 합치기만 하면 되지만, 타입의 경우보다 다양한 타입이 오기에 이를 재귀적으로 순환하며 속성을 merge하게 된다. 이 과정에서 호환되지 않는 타입들이 교차되는 등 특정 상황이 발생하여 TypeScript에서 값의 공집합을 뜻하는 never타입이 나올 수도 있다.

     

    type type1 = {a:1; b:2} & {b:3};

     

    특정 코드의 인터섹션 실행 결과. never이 나왔다.
    never이 나온다.

     

    이렇게 제대로 merge가 되지 않을 수도 있다. 객체에서만 쓰는 용도라면 인터페이스를 쓰는 것이 좋다.

     

     

    새 필드 추가

    type

    type Window = {
        title: string
    }
    
    type Window = {
        ts: TypeScriptAPI
    }
    
    // Error: Duplicate identifier 'Window'.
    // 병합되지 않는다. 고유해야 한다.

     

    interface

     선언적 확장이 가능하다.

    interface Window {
        title: string
    }
    
    interface Window {
        ts: TypeScriptAPI
    }
    
    const src = 'const a = "Hello World"';
    window.ts.transpileModule(src, {});

     

     

     편리한 확장이지만 한편으로는 동일 스코프에 동일한 이름의 타입을 중복으로 할당한다는 점이 문제 발생의 원인이 될 수 있으므로 ESLint로 변수 재선언을 허용하지 않는 no-redeclare을 활성화시킬수도 있다(해당 규칙으로 확인된 문제는 TS 컴파일러에서 자동으로 확인되지만 컴파일러 오류 메시지보다 ESLint 오류 메시지를 선호하는 경우에는 활성화시키면 된다).

     

    사용 범위

    type

    객체 O, 원시타입 O

    type AnObject1 = {
        value: string
    }
    
    // 객체 사용 가능
    
    type SanitizedString = string
    type EvenNumber = number
    
    // primitive 타입을 활용하여 사용자 정의 이름 만들기 가능

     

    interface

    객체 O, 원시타입 X

    interface AnObject1 {
        value: string
    }
    
    // 객체 사용 가능
    
    interface X extends string {}
    
    // interface는 primitive 타입을 활용한 사용자 정의가 가능하지 않다.

     

    Index Signature

    인덱스 시그니처(Index Signature)는 객체 내부에 [Key: T]: U 형식으로 선언하며, 해당 타입의 속성 키는 모두 T, 속성 값은 U를 가져야 함을 의미한다. 객체가 Key, Value의 형식, 각각의 타입을 명확히 해야 하는 경우 사용한다. 

     

    타입은 암묵적 인덱스 시그니처를 갖는다.

    // KnownAttributes가 type이라면 오류가 나지 않는다.
    interface KnownAttributes{
        x:number
        y:number
    }
    
    const knownAttributes: KnownAttributes={
        x:1,
        y:2
    }
    
    type RecordType = Record<string, number>;
    
    
    //Type 'KnownAttributes' is not assignable to type 'RecordType'.
    //Index signature for type 'string' is missing in type 'KnownAttributes'.
    const temp: RecordType = knownAttributes;

     

     

    인터페이스는 추후 확장될 수 있기에 인덱스 시그니처를 활용하려면 명시적으로 정의해줘야 한다.

     

    interface KnownAttributes {
        x:number
        y:number
        [z: number]:string;
    }
    
    const knownAttributes: KnownAttributes={
        x:1,
        y:2,
        1:'z'
    }
    
    
    const temp: KnownAttributes = knownAttributes;

     

     

     

    마지막으로 타입으로만 가능한 것들을 알아보자.

    type으로만 선언 가능한 타입들

    Union 타입

     

    교차 타입이 타입 A와 타입 B 모두 만족이라는 것이라면 유니온 타입은 OR연산자(||)처럼 타입 A거나 타입 B이다의 의미를 가진다. 

    type Fruit = 'apple' | 'lemon';
    type Vegetable = 'potato' | 'tomato';
    
    type Food = Fruit | Vegetable;
    // 'apple' | 'lemon' | 'potato' | 'tomato'
    
    // Union Type은 type alias를 통해서만 정의 가능하다.

     

    Tuple 타입

    튜플(Tuple)은 배열 타입을 특수하게 사용할 수 있는 타입이다. 배치 순서, 타입 모두 튜플에 명시된 대로 정의해야 한다. 

    type Animal = [name: string, age: number];
    const cat: Animal = ['', 1];
    
    // Tuple Type은 type alias를 통해서만 정의 가능하다.

     

     

    Mapped 타입

    맵드(Mapped) 타입은 기존 정의된 타입을 새로운 타입으로 변환해 주는 문법을 의미한다. JavaScript의 map 메서드를 생각하면 된다.

    type Vegetable = 'potato' | 'tomato';
    
    //type VegetableOption = {
    //    potato: boolean;
    //    tomato: boolean;
    //}
    type VegetableOption = {
        [Property in Vegetable]: boolean;
    }
    
    type VegetableOption = {
        [Property in Vegetable]: boolean;
    }
    
    const option: VegetableOption = {
    	potato: true,
        tomato: false,
    };
    
    // 'potato' | 'tomato'
    type VegetableAlias = keyof VegetableOption;

     

     

    Unknown 타입

     알려지지 않은 타입이 있다면 typeof를 사용하여 타입확인이 가능하다.

    const potato = {name: 'potato', weight: 1};
    
    // type Vegatable = {
    // name: string;
    // weight: number;
    // }
    type Vegetable = typeof potato;

     

     

     

    참고자료

    https://www.typescriptlang.org/ko/docs/handbook/2/everyday-types.html

    https://betterprogramming.pub/differences-between-type-aliases-and-interfaces-in-typescript-4-6-6489246d4e48

    https://yceffort.kr/2021/03/typescript-interface-vs-type

    https://devocean.sk.com/blog/techBoardDetail.do?ID=165230&boardType=techBlog

     

     

     

    댓글

🍀 Y0ungZ dev blog.
스크롤 버튼