ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TypeScript] 인터페이스
    TypeScript 2022. 9. 8. 22:56
    728x90

    소개

    TypeScript에서, 인터페이스는 이런 타입들의 이름을 짓는 역할을 하고 코드 안의 계약을 정의하는 것뿐만 아니라 프로젝트 외부에서 사용하는 코드의 계약을 정의하는 강력한 방법입니다.

    첫 번째 인터페이스 (Our First Interface)

    function printLabel(labeledObj: { label: string }) {
        console.log(labeledObj.label);
    }
    
    let myObj = {size: 10, label: "Size 10 Object"};
    printLabel(myObj);
    

    타입 검사는 printLabel 호출을 확인합니다. printLabel 함수는 string 타입 label을 갖는 객체를 하나의 매개변수로 가집니다. 이 객체가 실제로는 더 많은 프로퍼티를 갖고 있지만, 컴파일러는 필요한 프로퍼티가 있는지와 타입이 잘 맞는지만 검사합니다. TypeScript가 관대하지 않은 몇 가지 경우는 나중에 다루겠습니다.

    이번엔 같은 예제를, 문자열 타입의 프로퍼티 label을 가진 인터페이스로 다시 작성해 보겠습니다:

    interface LabeledValue {
        label: string;
    }
    
    function printLabel(labeledObj: LabeledValue){
        console.log(labeledObj.label);
    }
    
    let myObj = {size: 10, label: "Size 10 Object"};
    printLabel(myObj);
    

    LabeledValue 인터페이스는 이전 예제의 요구사항을 똑같이 기술하는 이름으로 사용할 수 있습니다. 이 인터페이스는 여전히 문자열 타입의 label 프로퍼티 하나를 가진다는 것을 의미합니다. 다른 언어처럼 printLabel에 전달한 객체가 이 인터페이스를 구현해야 한다고 명시적으로 얘기할 필요는 없고, 객체에 string 타입의 label이 존재하면 됩니다.

    타입 검사는 프로퍼티들의 순서를 요구하지 않습니다. 단지 인터페이스가 요구하는 프로퍼티들이 존재하는지와 프로퍼티들이 요구하는 타입을 가졌는지만을 확인합니다.

    선택적 프로퍼티(Optional Properties)

    선택적 프로퍼티를 가지는 인터페이스는 다른 인터페이스와 비슷하게 작성되고, 선택적 프로퍼티는 선언에서 프로퍼티 이름 끝에 ?를 붙여 표시합니다.

    interface SquareConfig {
        color?: string;
        width?: number;
    }
    
    function createSquare(config: SquareConfig): {color: string; area: number} {
        let newSquare = {color: "white", area: 100};
        if (config.color) {
            newSquare.color = config.color;
        }
        if (config.width) {
            newSquare.area = config.width * config.width;
        }
        return newSquare;
    }
    
    let mySquare = createSquare({color: "black"});
    

    읽기전용 프로퍼티(Readonly properties)

    객체가 처음 생성될 때 외에 수정할 수 없어야 할 때 사용ㅎ랍니다.

    interface Point {
        readonly x: number;
        readonly y: number;
    }
    
    let p1: Point = { x: 10, y: 20 };
    p1.x = 5; // 오류!
    

    배열에도 readonly를 사용할 수 있습니다. readonly Array의 경우 재할당도 불가능합니다.

    let a: number[] = [1, 2, 3, 4];
    let ro: ReadonlyArray<number> = a;
    ro[0] = 12; // 오류!
    ro.push(5); // 오류!
    ro.length = 100; // 오류!
    a = ro; // 오류!
    

    하지만 타입 단언으로 오버라이드 하는것은 가능합니다.

    a = ro as number[];
    
    • readonly vs const
      • 변수는 const를 사용하고 프로퍼티는 readonly를 사용합니다

    초과 프로퍼티 검사(Excess Property Checks)

    대상 타입이 갖고 있지 않은 프로퍼티를 갖고 있으면 에러가 발생합니다.

    interface SquareConfig {
        color?: string;
        width?: number;
    }
    
    function createSquare(config: SquareConfig): { color: string; area: number } {
        // ...
    }
    
    let mySquare = createSquare({ colour: "red", width: 100 });
    

    colour를 갖고있지 않기 때문에 에러가 발생하는데 이 검사를 피하는 가장 간단한 방법은 타입 단언을 사용하는 것입니다.

    let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
    

    하지만 특별한 경우에, 추가 프로퍼티가 있음을 확신한다면, 문자열 인덱스 서명(string index signatuer)을 추가하는 것이 더 나은 방법입니다.

    interface SquareConfig {
        color?: string;
        width?: number;
        [propName: string]: any;
    }
    

    하지만 이러한 경우에도 변수가 공통 객체 프로퍼티가 없다면 에러가 발생합니다.

    let squareOptions = { colour: "red" };
    let mySquare = createSquare(squareOptions);
    

    함수 타입(Function Types)

    매개변수 목록과 반환 타입만 주어진 함수 선언과 비슷하게 인터페이스로 함수 타입을 기술할 수 있습니다.

    interface SearchFunc {
        (source: string, subString: string): boolean;
    }
    
    • 함수 타입의 변수를 만들고, 같은 타입의함수 값으로 할당
    • let mySearch: SearchFunc; mySearch = function(source: string, subString: string) { let result = source.search(subString); return result > -1; }
    • 매개변수의 이름이 같을 필요는 없습니다.
    • let mySearch: SearchFunc; mySearch = function(src: string, sub: string): boolean { let result = src.search(sub); return result > -1; }
    • 매개변수에 타입을 지정하지 않아도 Typescript가 타입을 추론할 수 있습니다.
    • let mySearch: SearchFunc; mySearch = function(src, sub) { let result = src.search(sub); return result > -1; }
    • 하지만 추론된 타입과 다른 매개변수로 사용된거나 반환되는 결과가 다르다면 에러가 발생합니다.
    • let mySearch: SearchFunc; // error: Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'. // Type 'string' is not assignable to type 'boolean'. mySearch = function(src, sub) { let result = src.search(sub); return "string"; };

    인덱서블 타입(Indexable Types)

    인덱서블 타입이란, 인덱싱이 가능한 자료형을 의미합니다. 인덱서블 타입은 인덱스 시그니쳐를 가져야하며, 인덱스 시그니쳐는 number 혹은 string이어야 합니다.

    a[10] 이나 ageMap["daniel"]처럼 타입을 "인덱스로" 기술할 수 있습니다.

    interface StringArray {
        [index: number]: string;
    }
    
    let myArray: StringArray;
    myArray = ["Bob", "Fred"];
    
    let myStr: string = myArray[0];
    

    js에선 number를 string으로 저장하기때문에 string이 number보다 더 큰 범주입니다..

    class Animal {
        name: string;
    }
    class Dog extends Animal {
        breed: string;
    }
    
    // 오류: 숫자형 문자열로 인덱싱을 하면 완전히 다른 타입의 Animal을 얻게 될 것입니다!
    interface NotOkay {
        [x: number]: Animal;
        [x: string]: Dog;
    }
    

    따라서 위의 코드처럼 정의한다면 array[100] = array[”100”]이 되기 때문에 오류가 발생합니다.

    문자열 인덱스 시그니처는 "사전" 패턴을 기술하는데 강력한 방법이지만, 모든 프로퍼티들이 반환 타입과 일치하도록 강제합니다. 문자열 인덱스가 obj.property가 obj["property"]로도 이용 가능함을 알려주기 때문입니다. 다음 예제에서, name의 타입은 문자열 인덱스 타입과 일치하지 않고, 타입 검사는 에러를 발생시킵니다.

    interface NumberDictionary {
        [index: string]: number;
        length: number;    // 성공, length는 숫자입니다
        name: string;      // 오류, `name`의 타입은 인덱서의 하위타입이 아닙니다
    }
    

    하지만, 인덱스 시그니처가 합집합이라면 집합내에 포함되는 다른 타입도 사용 가능합니다.

    interface NumberOrStringDictionary {
        [index: string]: number | string;
        length: number;    // 성공, length는 숫자입니다
        name: string;      // 성공, name은 문자열입니다
    }
    

    추가적인 인덱스 할당을 막기 위해 읽기 전용으로 만들 수 있습니다.

    interface ReadonlyStringArray {
        readonly [index: number]: string;
    }
    let myArray: ReadonlyStringArray = ["Alice", "Bob"];
    myArray[2] = "Mallory"; // 오류!
    

    클래스 타입(Class Types)

    인터페이스 구현하기(Implementing an interface)

    클래스가 특정 계약을 충족시키도록 명시적으로 강제할 수 있습니다.

    interface ClockInterface {
        currentTime: Date;
    }
    
    class Clock implements ClockInterface {
        currentTime: Date = new Date();
        constructor(h: number, m: number) { }
    }
    

    아래 예제의 setTime 처럼 클래스에 구현된 메서드를 인터페이스 안에서도 기술할 수 있습니다.

    interface ClockInterface {
        currentTime: Date;
        setTime(d: Date): void;
    }
    
    class Clock implements ClockInterface {
        currentTime: Date = new Date();
        setTime(d: Date) {
            this.currentTime = d;
        }
        constructor(h: number, m: number) { }
    }
    

    인터페이스는 클래스의 public과 private 모두보다는, public을 기술합니다. 그래서 클래스 인스턴스의 private에서는 특정 타입이 있는지 검사할 수 없습니다.

    클래스의 스태틱과 인스턴스의 차이점(Difference between the static and instance sides of classes)

    클래스는 스태틱 타입과 인스턴스 타입 두 가지 타입을 갖습니다.

    생성 시그니처로 인터페이스를 생성하고, 클래스를 생성하려고 한다면, 인터페이스를 implements 할 때, 에러가 발생하는 것을 확인할 수 있습니다.

    interface ClockConstructor {
        new (hour: number, minute: number);
    }
    
    // Class 'Clock' incorrectly implements interface 'ClockConstructor'.
    // Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.
    class Clock implements ClockConstructor {
        currentTime: Date;
        constructor(h: number, m: number) { }
    }
    출처: <https://kjwsx23.tistory.com/451> [香格里拉:티스토리]
    

    클래스가 인터페이스를 implements할 때, 클래스의 instance side만 체크됩니다. 클래스의 constructor는 static side이기 때문에, 체크되지 않습니다.

    따라서 아래와 같이 분리해주는 것이 좋습니다.

    interface ClockConstructor {
        new (hour: number, minute: number): ClockInterface;
    }
    interface ClockInterface {
        tick(): void;
    }
    
    function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
        return new ctor(hour, minute);
    }
    
    class DigitalClock implements ClockInterface {
        constructor(h: number, m: number) { }
        tick() {
            console.log("beep beep");
        }
    }
    class AnalogClock implements ClockInterface {
        constructor(h: number, m: number) { }
        tick() {
            console.log("tick tock");
        }
    }
    
    let digital = createClock(DigitalClock, 12, 17);
    let analog = createClock(AnalogClock, 7, 32);
    
    • ClockConstructor : 생성자를 정의
    • ClockInterface : 인스턴스 메서드를 정의하는 두 인터페이스를 정의
    • createClock : 전달된 타입의 인스턴스를 생성을 정의

    createClock의 첫 번째 매개변수는 createClock(AnalogClock, 7, 32)안에 ClockConstructor 타입이므로, AnalogClock이 올바른 생성자 시그니처를 갖고 있는지 검사합니다.

    또 다른 쉬운 방법으로는 클래스 표현을 사용합니다.

    interface ClockConstructor {
      new (hour: number, minute: number);
    }
    
    interface ClockInterface {
      tick();
    }
    
    const Clock: ClockConstructor = class Clock implements ClockInterface {
      constructor(h: number, m: number) {}
      tick() {
          console.log("beep beep");
      }
    }
    

     

    참고

    https://typescript-kr.github.io/pages/interfaces.html

    https://kjwsx23.tistory.com/451

    728x90

    'TypeScript' 카테고리의 다른 글

    [TypeScript] 제네릭  (0) 2022.09.17
    [TypeScript] 열거형  (1) 2022.09.16
    [TypeScript] 클래스  (0) 2022.09.15
    [TypeScript] 유니언과 교차타입  (1) 2022.09.10
    [TypeScript] 기본 타입  (2) 2022.09.07

    댓글

Designed by Tistory.