TypeScript 6장 - 러닝 타입스크립트(5)
포스트
취소

TypeScript 6장 - 러닝 타입스크립트(5)

TypeScript

image

Part.2

chapter.7 인터페이스

7.1 타입 별칭 vs. 인터페이스

  • born: number와 name: string을 가진 객체를 타입 별칭으로 구현하는 간략한 구문이다.

    1
    2
    3
    4
    
    type Poet = {
      born: number;
      name: string;
    };
    
  • 인터페이스로 구현한 동일한 구문이며 두 구문은 거의 같다.
  • 세미콜론을 사용해 변수를 선언하는 것과 세미콜론 없이 클래스 또는 함수를 선언하는 것의 차이를 보인다.

    1
    2
    3
    4
    
    interface Poet {
      born: number;
      name: string;
    }
    
  • 인터페이스에 대한 타입스크립트의 할당 가능성 검사와 오류 메시지는 객체 타입에서 실행되는 것과 거의 동일하다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    let valueLater: Poet;
    
    valueLater = {
      born: 1954,
      name: "Sara",
    };
    
    valueLater = "Emi";
    // Error: Type 'string' is not assignable to 'Poet'.
    
    valueLater = {
      born: true,
      // Error: Type 'boolean' is not assignable to 'number'.
      name: "Saa",
    };
    
    • 인터페이스와 타입 별칭 사이에는 몇 가지 주요 차이점이 있다.
      • 인터페이스는 속성 증가를 위해 병할 수 있다.
      • 내장돈 전역 인터페이스 또는 npm패키지와 같은 외부 코드를 사용할 때 특히 유용하다.
      • 인터페이스는 클래스가 선언된 구조의 타입을 확인하는 데 사용할 수 있지만 타입 별칭은 사용할 수 없다.
      • 인터페이스에서 타입스크립트 타입 검사기가 더 빨리 작동한다.
      • 인터페이스는 타입 별칭이 하는 것처럼 새로운 객체 리터럴의 동적인 복사 붙여넣기보다 내부적으로 더 쉽게 캐시할 수 있는 명명된 타입을 선언한다.
      • 인터페이스는 이름 없는 객체 리터럴의 별칭이 아닌 이름 있는 객체로 간주되므로 어려운 특이 케이스에서 나타나는 오류 메시지를 좀 더 쉽게 읽을 수 있다.

7.2 속성 타입

7.2.1 선택적 속성
  • 객체 타입과 마찬가지로 모든 객체가 필수적으로 인터페이스 속성을 가질 필요는 없다.
  • 타입 애너테이션 : 앞에 ?를 사용해 인터페이스의 속성이 선택적 속성임을 나타낼 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    interface Book {
      author?: string;
      pages: number;
    }
    
    const ok: Book = {
      author: "Rita",
      pages: 70,
    };
    
    const missing: Book = {
      pages: 70,
    };
    
    • Book 인터페이스는 필수 속성 pages와 선택적 속성 author를 갖는다.
    • 선택적 속성은 생략할 수 있다.
7.2.2 읽기 전용 속성
  • 타입스크립트는 속성 이름 앞에 readonly 키워드를 추가해 다른 값으로 설정될 수 없음을 나타낸다.
  • readonly 속성은 평소대로 읽을 수 있지만 새로운 값으로 재할당하지못한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    interface Page {
      readonly text: string;
    }
    
    function read(page: Page) {
      console.log(page.text); // OK
    
      page.text += "!";
      // Error: Cannot assign to 'text' because it is a read-only property.
    }
    // 타입스크립트에서 타입 검사를 위해 선언된 것이기 때문에 Page를 따로 사용할 수 없다.
    
    • readonly 제한자는 타입 시스템에만 존재하며 인터페이스에만 사용할 수 있다.
    • readonly 제한자는 객체의 인터페이스를 선언하는 위치에서만 사용되고 실제 객체에는 적용되지 않는다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    const pageIsh = {
      text: "Hello, world!",
    };
    
    pageIsh.text += "!"; // Ok
    // pageIsh는 객체가 아니라 text가 있는, 유추된 객체 타입이다.
    
    read(pageIsh); // OK
    // pageIsh의 더 구체적인 버전인 Page를 읽는다.
    
    • Page 에제에서 text 속성의 부모 객체는 함수 내부에서 text로 명시적으로 사용되지 않았기 때문에 함수 밖에서 속성을 수정할 수 있다.
    • readonly 속성에 할당할 수 있으므로 pageIsh는 Page로 사용할 수 있다.
    • 쓰기 가능한 속성은 readonly 속성이 필요한 모든 위치에서 읽을 수 있다.
    • 명시적 타입 애너테이션인 : Page로 변수 pageIsh를 선언하면 타입스크립트에 text 속성이 readonly라고 가리키게 된다.
      • 하지만 유추된 타입은 readonly가 아니다.
    • readonly 인터페이스 멤버는 코드 영역에서 객체를 의도치 않게 수정하는 것을 막는 편리한 방법이다.
    • readonly는 타입 시스템 구성 요소일 뿐 컴파일된 자바스크립트 출력 코드에는 존재하지 않는다.
    • readonly는 타입스크립트 타입 검사기를 사용해 개발 중에 속성이 수정되지 못하도록 보호하는 역할을 한다.
7.2.3 함수와 메서드
  • 타입스크립트에서 인터페이스 멤버를 함수 타입으로 선언할 수 있다.
  • 인터페이스 멤버를 함수로 선언하는 두 가지 방법을 제공한다.

    • 메서드 구문
      • 인터페이스 멤버를 member(): void와 같이 객체의 멤버로 호출되는 함수로 선언
    • 속성 구문
      • 인터페이스의 멤버를 member(): => void와 같이 독립 함수와 동일하게 선언
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    interface HasBothFunctionTypes {
      property: () => string;
      method(): string;
    }
    
    const hasBoth: HasBothFunctionTypes = {
      property: () => "",
      method() {
        return "";
      },
    };
    
    hasBoth.property(); // OK
    hasBoth.method(); // OK
    
    • 두 가지 방법 모두 선택적 속성 키워드인 ?를 사용해 필수로 제공하지 않아도 되는 멤버로 나타낼 수 있다.
    1
    2
    3
    4
    
    interface OptionalReadOnlyFunctions {
      optionalProperty: () => string;
      optionalMethod?(): string;
    }
    
    • 메서드와 속성 선언은 서로 교환하여 사용할 수 있다.
    • 메서드와 속성의 주요 차이점은 다음과 같다.
      • 메서드는 readonly로 선언할 수 없지만 속성은 가능하다.
      • 인터페이스 병합은 메서드와 속성을 다르게 처리한다.
    • 타입스크립트의 향후 버전에서는 메서드와 속성 함수의 차이점에 대해 더 엄격한 옵션을 추가할지 모르기 때문에 추천하는 스타일 가이드는 다음과 같다.
      • 기본 함수가 this를 참조할 수 있다는 것을 알고 있다면 메서드 함수를 사용한다.
      • 가장 일반적으로 클래스의 인스턴스에서 사용된다.
      • 반대의 경우는 속성 함수를 사용한다.
7.2.4 호출 시그니처
  • 인터페이스와 객체 타입은 호출 시그니처로 선언할 수 있다.
  • 호출 시그니처는 값을 함수처럼 호출하는 방식에 대한 타입 시스템의 설명이다.
  • 호출 시그니처가 선언한 방식으로 호출되는 값만 인터페이스에 할당할 수 있다.
  • 호출 시그니처는 함수 타입과 비슷하지만, 콜론(:) 대신 화살표(=>)로 표시한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    type FunctionAlias = (input: string) => number;
    
    interface CallSignature {
      (input: string): number;
    }
    
    const typedFunctionAlias: FunctionAlias = (input) => input.length; // OK
    // 타입: (input: string) => number
    const typedCallSignature: CallSignature = (input) => input.length; // OK
    
    • FunctionAlias와 CallSignature 타입은 모두 동일한 매개변수와 반환 타입을 설명한다.
  • 호출 시그니처는 사용자 정의 속성을 추가로 갖는 함수를 설명하는 데 사용할 수 있다.
  • 함수 선언에 추가된 속성을 해당 함수 선언의 타입에 추가하는 것으로 인식한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    interface FunctionWithCount {
      count: number;
      (): void;
    }
    
    let hasCallCount: FunctionWithCount;
    
    function keepsTrackOfCalls() {
      keepsTrackOfCalls.count += 1;
      console.log(`I've been called ${keepsTrackOfCalls.count} times!`);
    }
    
    keepsTrackOfCalls.count = 0;
    
    hasCallCount = keepsTrackOfCalls; // OK
    
    function doesNotHaveCount() {
      console.log("No idea!");
    }
    
    hasCallCount = doesNotHaveCount;
    // Error: Property 'count' is missing in type
    // '() => void' but required in type 'FunctionWithCalls'
    
    • keepsTrackOfCalls 함수 선언에는 number 타입인 counter 속성이 주어져 FunctionWithCount 인터페에시으 할당할 수 있다.
    • FucntionWithCount 타입의 hasCallCount 인수에 할당할 수 있다.
7.2.5 인덱스 시그니처
  • 일부 자바스크립트 프로젝트는 임의의 string 키에 값을 저장하기 위한 객체를 생성한다.
  • 타입스크립트는 인덱스 시그니처 구문을 제공해 인터페이스의 객체가 임의의 키를 받고, 해당 키 아래의 특정 타입을 반환할 수 있음을 나타낸다.
  • 자바스크립트 객체 속성 조회는 암묵적으로 키를 문자열로 변환하기 때문에 인터페이스의 객체는 문자열 키와 함께 가장 일반적으로 사용된다.
  • 인덱스 시그니처는 일반 속성 정의와 유사하지만 키 다음에 타입이 있고 {[i: string]: …}과 같이 배열의 대괄호를 갖는다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    interface WordCounts {
      [i: string]: number;
    }
    
    const counts: WordCounts = {};
    
    counts.apple = 0; // OK
    counst.banana = 1; // OK
    
    counts.cherry = false;
    // Error: Type 'boolean' is not assignable to type 'number'.
    
    • WordCounts 인터페이스는 number 값을 갖는 모든 string 키를 허용하는 것으로 선언되어 있다.
    • 값이 number면 string 키가 아닌 그 어떤 키도 바인딩할 수 없다.
  • 인덱스 시그니처는 객체에 값을 할당할 때 편리하지만 타입 안정성을 완벽하게 보장하지는 않는다.
  • 인덱스 시그니처는 객체가 어떤 속성에 접근하든 간에 값을 반환해야 함을 나타낸다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    interface DatesByName {
      [i: string]: Date;
    }
    
    const publishDates: DatesByName = {
      Frankenstein: new Date("1 January 1818"),
    };
    
    publishDates.Frankenstein; // 타입: Date
    console.log(publishDates.Frankenstein.toString()); // OK
    
    publishDates.Beloved; // 타입은 Date이지만 런타임 값은 undefined
    console.log(publishDates.Beloved.toString());
    // 타입 시스템에서는 오류가 나지 않지만 실제 런타입에서는 오류가 발생함
    // Runtime error: Cannot read property 'toString'
    // of undefined (reading publishDates.Beloved)
    
    • publishedDates 값은 Date 타입으로 Frankenstein을 안전하게 반환하지만 타입스크립트는 Beloved가 정의되지 않았음에도 정의되었다고 생각하도록 속인다.
    • 키/값 쌍을 저장하려고 하는데 키를 미리 알 수 없다면 Map을 사용하는 편이 더 안전하다.
    • .get 메서드는 항상 키가 존재하지 않음을 나타내기 위해 | undefined 타입을 반환한다.
  • 속성과 인덱스 시그니처 혼합

    • 인터페이스는 명시적으로 명명된 속성과 포괄적인 용도의 string 인덱스 시그니처를 한번에 포함할 수 있다.
    • 각각의 명명된 속성의 타입은 포괄적인 용도의 인덱스 시그니처로 할당할 수 있어야 한다.
    • 명명된 속성이 더 구체적인 타입을 제공하고, 다른 모든 속성은 인덱스 시그니처의 타입으로 대체하는 것으로 혼합해서 사용할 수 있다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      
      interface HistoricalNovels {
        Oroonoko: number;
        [i: string]: number;
      }
      
      const novels: HistNovels = {
        Outlander: 1991,
        Oroonoko: 1688,
      }; // OK
      
      const missingOroonoko: HistoricalNovels = {
        // Error: Property 'Oroonoko' is missing in type
        // '{Outlander: number;}' but required in type 'HistoricalNovels'.
        Outlander: 1991,
      };
      // Oroonoko 속성은 반드시 필요하다.
      
    • 속성과 인덱스 시그니처를 혼합해서 사용하는 일반적인 타입 시스템 기법 중 하나는 인덱스 시그니처의 원시 속성보다 명명된 속성에 대해 더 구체적인 속성 타입 리터럴을 사용하는 것이다.
    • 명명된 속성의 타입이 인덱스 시그니처에 할당될 수 있는 경우(각각의 리터럴 및 원시 속성에 해당) 타입스크립트는 더 구체적인 속성 타입 리터럴을 사용하는 것을 허용한다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      
      interface ChapterStarts {
        perface: 0;
        [i: string]: number;
      }
      
      const correctPreface: ChapterStarts = {
        preface: 0,
        night: 1,
        shopping: 5,
      };
      
      const wrongPreface: ChapterStarts = {
        preface: 1,
        // Error: Type '1' is not assignable to type '0'
      };
      
      • preface 속성은 0으로, 다른 모든 속성은 더 일반적인 number를 갖도록 선언한다.
      • ChapterStarts를 사용하는 모든 객체의 preface속성은 반드시 0이어야 한다.
  • 숫자 인덱스 시그니처

    • 타입스크립트 인덱스 시그니처는 키로 string 대신 number 타입을 사용할 수 있지만, 명명된 속성은 그 타입을 포괄적인 용도의 string 인덱스 시그니처의 타입으로 할당할 수 있어야 한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    interface MoreNarrowNumbers {
      [i: number]: string;
      [i: string]: string | undefined;
    } // OK
    
    const mixesNumbersAndStrings: MoreNarrowNumbers = {
      0: "",
      key1: "",
      key2: undefined,
    };
    
    interface MoreNarrowStrings {
      [i: number]: string | undefined;
      // Error: 'number' index type 'string|undefined'
      // is not assignable to 'string' index type 'string'.
      [i: string]: stirng;
    }
    
7.2.6 중첩 인터페이스
  • 객체 타입이 다른 객체 타입의 속성으로 중첩될 수 있는 것처럼 인터페이스 타입도 자체 인터페이스 타입 혹은 객체 타입을 속성으로 가질 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    interface Novel {
      author: {
        name: string;
      };
      setting: Setting;
    }
    
    interface Setting {
      place: string;
      year: number;
    }
    
    let myNovel: Novel;
    
    myNovel = {
      author: {
        name: "Jane",
      },
      setting: {
        place: "England",
        year: 1812,
      },
    };
    
    myNovel = {
      author: {
        name: "Emily",
      },
      setting: {
        // Error: Property 'year' is missing in type
        // '{place: string}' but required in type 'Setting'.
        place: "West",
      },
    };
    

7.3 인터페이스 확장

  • 다른 인터페이스의 모든 멤버를 포함하고, 거기에 몇 개의 멤버가 추가된 인터페이스의 경우 서로 형택가 비슷한 여러 개의 인터페이스를 갖게 된다.
  • 타입스크립트는 인터페이스가 다른 인터페이스의 모든 멤버를 복사해서 선언할 수 있는 확장된 인터페이스를 허용한다.
  • 확장할 인터페이스의 이름 뒤에 extends 키워드를 추가해서 다른 인터페이스를 확장한 인터페이스라는 걸 표시한다.
  • 파생 인터페이스를 준수하는 모든 객체가 기본 인터페이스의 모든 멤버도 가져야 한다는 것을 타입스크립트에 알려준다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
    interface Writing {
      title: string;
    }
    
    interface Novella extends Writing {
      pages: number;
    }
    
    let myNovella: Novella = {
      pages: 196,
      title: "Ethan",
    }; // Ok
    
    let missingPages: Novella = {
      // Error: Property 'pages' is missing in type '{title: string}' but
      // required in type 'Novella'.
      title: "The",
    };
    
    let extraProperty: Novella = {
      // Error: Type '{pages: number; strategy: string; style: string}'
      // is not assignable to type 'Novella'.
      // Object literal may only specify known properties
      // and 'strategy' does not exist in type 'Novella'.
      pages: 300,
      strategy: "baseline",
      style: "Basic",
    };
    
    • 인터페이스 확장은 프로젝트의 한 엔티티 타입이 다른 엔티티의 모든 멤버를 포함하는 사위 집합을 나타내는 실용적인 방법이다.
    • 인터페이스 확장 덕분에 여러 인터페이스에 관계를 나타내기 위해 동일한 코드를 반복 입력하는 작업을 피할 수 있다.

7.3.1 재정의된 속성

  • 파생 인터페이스는 다른 타입으로 속성을 다시 선언해 기본 인터페이스의 속성을 재정의하거나 대체할 수 있다.
  • 타입스크립트의 타입 검사기는 재정의된 속성이 기본 속성에 할당되어 있도록 강요한다.
  • 속성을 재선언하는 대부분의 파생 인터페이스는 해당 속성을 유니언 타입의 더 구체적인 하위 집합으로 만들거나 속성을 기본 인터페이스의 타입에서 확장된 타입으로 만들기 위해 사용한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    interface WithNullableName {
      name: string | null;
    }
    
    interface WithNonNullableName extends WithNullableName {
      name: string;
    }
    
    interface WithNumericName extends WithNullableName {
      // Error: Interface 'WithNumbericName' incorrectly interface
      // 'WithNullableName'
      // Types of property 'name' are incompatible.
      // Type 'string|number' is not assignable to type 'string|null'.
      // Type 'number' is not assignable to type 'string'.
      name: number | string;
    }
    
    • WithNullableName 타입은 WithNonNullableName에서 null을 허용하지 않도록 적절하게 다시 설정된다.
    • WithNumericName 의 name에는 number|string이 허용되지 않는다.
    • number|stringstring|null에 할당할 수 없다.
7.3.2 다중 인터페이스 확장
  • 타입스크립트의 인터페이스는 여러 개의 다른 인터페이스를 확장해서 선언할 수 있다.
  • 파생 인터페이스 이름에 extends 키워드 뒤에 쉼표로 인터페이스 이름을 구분해 사용하면 된다.
  • 파생 인터페이스는 모든 기본 인터페이스의 모든 멤버를 받는다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    interface GivesNumber {
      giveNumber(): number;
    }
    
    interface GivesString {
      giveString(): string;
    }
    
    interface GivesBothAndEither extends GivesNumber, GivesString {
      giveEither(): number | string;
    }
    
    function useGivesBoth(instance: GivesBothAndEither) {
      instance.giveEither(); // 타입: number|string
      instance.giveNumber(); // 타입: number
      instance.giveString(); // 타입: string
    }
    
    • 여러 인터페이스를 확장하는 방식으로 인터페이스를 사용하면 코드 중복을 줄이고 다른 코드영역에서 객체의 형태를 더 쉽게 재사용할 수 있다.

7.4 인터페이스 병합

  • 인터페이스의 중요한 특징 중 하나는 서로 병합하는 능력이다.
  • 두 개의 인터페이스가 동일한 이름으로 동일한 스코프에 선언된 경우, 선언된 모든 필드를 포함하는 더 큰 인터페이스가 코드에 추가된다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    interface Merged {
      fromFirst: string;
    }
    
    interface Merged {
      fromSecond: number;
    }
    // interface Merged {
    //   fromFirst: string;
    //   fromSecond: number;
    // }
    
    • 두 개의 속성을 갖는 Merged 인터페이스를 각각 선언하여, 하나의 인터페이스로 병합된다.
  • 일반적인 타입스크립트 개발에서 인터페이스 병합을 자주 사용하지는 않는다.
  • 인터페이스가 여러 곳에 선언되면 코드를 이해하기가 어려워지므로 가능하면 인터페이스 병합을 사용하지 않는 것이 좋다.
  • 인터페이스 병합은 외부 패키지 또는 Window 같은 내장된 전역 인터페이스를 보강하는 데 유용하다.
  • 타입스크립트 컴파일러 옵션을 사용할 때, 파일에서 myEnviromentVariable 속성이 있는 Window 인터페이스를 선언하면 window.myEnviromentVariable을 사용할 수 있다.

    1
    2
    3
    4
    5
    
    interface Window {
      myEnviromentVariable: string;
    }
    
    window.myEnviromentVariable; // 타입: string
    
7.4.1 이름이 충돌되는 멤버
  • 병합돈 인터페이스는 타입이 다른 동일한 이름의 속성을 여러 번 선언할 수 없다.
  • 속성이 이미 인터페이스에 선언되어 있다면 나중에 병합된 인터페이스에서도 동일한 타입을 사용해야 한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    interface MergedProperties {
      same: (input: boolean) => string;
      different: (input: string) => string;
    }
    
    interface MergedProperties {
      same: (input: boolean) => string; // OK
    
      different: (input: number) => string;
      // Error: Subsequent property declarations must have the same type.
      // Property 'different' must be of type '(input: number) => string'.
      // but here has type '(input: number) => string'.
    }
    
    • MergedProperties 인터페이스 선언에서는 same 속성이 모두 동일하므로 문제 없지만 different 속성은 타입이 서로 다르기 때문에 오류가 발생한다.
  • 병합된 인터페이스는 동일한 이름과 다른 시그니처를 가진 메서드는 정의할 수 있다.
  • 이렇게 하면 메서드에 대한 함수 오버로드가 발생한다.

    1
    2
    3
    4
    5
    6
    7
    
    interface MergedMethods {
      different(input: string): string;
    }
    
    interface MergedMethods {
      different(input: number): string; // OK
    }
    
    • MergedMethods 인터페이스는 두 가지 오버로드가 있는 different 메서드를 생성한다.

7.5 마치며

  • 타입 별칭 대신 인터페이스를 사용한 객체 타입을 선언할 수 있다.
  • 선택적 속성, 읽기 전용 속성, 함수, 메서드 등 다양한 인ㅁ터페이스 속성 타입
  • 객체의 포괄적인 속성을 담기 위한 인덱스 시그니처 사용하기
  • 중첩된 인터페이스와 extends 상속 확장으로 인터페이스 재사용하기
  • 동일한 이름의 인터페이스 병합하기
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.