러닝 타입스크립트 | ch 7. 인터페이스

doodoo·2023년 3월 5일
1
post-thumbnail

인터페이스

  • 인터페이스는 연관된 이름으로 객체 형태를 설명하는 또 다른 방법
  • 타입 별칭으로 된 객체타입과 비슷하지만 다음과 같은 이유로 더 선호된다.
    • 읽기 쉬운 오류 메시지
    • 더 빠른 컴파일 성능
    • 클래스와의 더 나은 상호 운용성

타입 별칭 vs. 인터페이스

아래는 name과 id를 가지는 객체를 각각 타입 별칭과 인터페이스로 구현한 예시

// 타입 별칭 
type User = {
    name: string;
    id: number;
};
// 인터페이스 
interface User {
    name: string;
    id: number;
}

📌 타입 별칭 뒤에는 세미콜론이 붙고 인터페이스에는 붙지 않는다. 이것은 세미콜론을 사용해 변수를 선언하는 것과 세미콜론 없이 클래스 또는 함수를 선언하는 것의 차이를 반영한것


차이점

타입별칭과 인터페이스는 비슷해보이지만 주요한 차이점이 몇 가지 있다.

  • 인터페이스는 병합이 가능하다.
  • 인터페이스는 클래스가 선언된 구조의 타입을 확인하는 데 사용할 수 있지만 타입 별칭은 사용할 수 없다.
  • 인터페이스에서 타입스크립트 타입 검사기가 더 빨리 작동한다.
  • 인터페이스의 오류 메시지가 좀 더 읽기 쉽다.

👍 타입 별칭의 유니언 타입이 필요한게 아니라면 인터페이스 사용을 추천함



속성 타입

📌 속성 타입은 별칭 객체 타입에서도 사용할 수 있다.

선택적 속성

  • 모든 객체가 인터페이스 속성을 가질 필요는 없다.
  • 타입 애너테이션 : 앞에 ?를 붙이면 인터페이스의 속성이 선택적 속성임을 나타낸다.
interface Book {
    author?: string; // 옵션 (생략 가능)
    pages: number; // 필수 
}


// OK
const ok: Book = {
    author: "Rita Dove",
    pages: 80,
}

const missing: Book = {
    pages: 80
}

읽기 전용 속성

  • 속성 이름 앞에 readonly 키워드를 사용해서 속성을 읽기 전용으로 만들 수 있다.
  • 읽기 전용 속성은 읽기만 가능, 재할당 불가
  • readonly는 타입 시스템에만 존재하며 인터페이스에서만 사용할 수 있다.
interface Page {
    readonly text: string;
}

function read(page: Page) {
    // OK: 속성을 수정하지 않고 읽는것 
    console.log(page.text);

    // Error: Cannot assign to 'text' because it is a read-only property.
    page.text += "!";
}

함수와 메서드

인터페이스 멤버를 함수로 선언하는 두 가지 방법
1. 메서드 구문: member(): void와 같이 객체의 멤버로 호출되는 함수로 선언
2. 속성 구문: member: () => void와 같이 독립 함수와 동일하게 선언

interface HasBothFunctionTypes {
  property: () => string;
  method(): string;
}

const hasBoth: HasBothFunctionTypes = {
  property: () => "",
  method() {
    return "";
  },
};

hasBoth.property();
hasBoth.method();

? 를 사용해 선택적 속성 멤버로 나타낼 수 있다.

interface OptionalReadonlyFunctions {
  optionalProperty?: () => string;
  optionalMethod?(): string;
}

메서드와 속성의 차이점

메서드와 속성 선언은 대부분 서로 교환해서 사용할 수 있으나 몇 가지 차이점이 있다.

  • 메서드는 readonly로 선언할 수 없지만 속성은 가능함
  • 인터페이스 병합은 메서드와 속성을 다르게 처리함
  • 타입에서 수행되는 일부 작업은 메서드와 속성을 다르게 처리함


호출 시그니처

호출 시그니처는 함수의 매개변수와 반환 타입을 지정한다.

// 둘 다 동일한 함수 매개변수와 반환 타입을 설명함 

type FunctionAlias = (input: string) => number;

interface CallSignature {
  (input: string): number;
}

const typeFunctionAlias: FunctionAlias = input => input.length; // OK

const typeCallSignature: CallSignature = input => input.length; // OK

호출 시그니처를 사용하면 사용자 정의 속성을 갖는 함수를 설명할 수 있다.

interface FunctionWithCount {
  count: number;
  (): void;
}

let hasCallCount: FunctionWithCount;

function keepsTrackOfCalls() {
  keepsTrackOfCalls.count += 1;
  console.log(`I've been called ${keepsTrackOfCalls.count} types!`);
}

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 'FunctionWithCount'.

인덱스 시그니처

  • 객체에 타입을 지정해야 할 속성이 많을때 인덱스 시그니처를 사용할 수 있다.
  • 인덱스 시그니처 구문은 인터페이스의 객체가 임의의 키를 받고, 특정 타입을 반환할 수 있음을 나타낸다.
  • 객체는 키를 암묵적으로 문자열로 변환하기 때문에 일반적으로 인터페이스의 객체는 문자열 키와 함께 사용된다.
// WordCounts 인터페이스는 number 타입의 값을 갖는 모든 string 키를 허용한다. 
interface WordCounts {
  [i: string]: number;
}

const counts: WordCounts = {};

counts.apple = 0; // OK
counts.banana = 1; // OK

counts.cherry = false; // Error: Type 'boolean' is not assignable to type 'number'.

속성과 인덱스 시그니처 혼합

인덱스 시그니처는 명시적으로 명명된 속성과 포괄적인 용도의 string 인덱스 시그니처를 한 번에 포함할 수 있다.

interface HistoricalNovels {
  Oroonoko: number;
  [i: string]: number;
}

const novels: HistoricalNovels = {
  Outlander: 1991,
  Oroonoko: 1688,
};

const missingOroonoko: HistoricalNovels = {
  Outlander: 1991,
};
// Error: Property 'Oroonoko' is missing in type '{ Outlander: number; }' 
// but required in type 'HistoricalNovels'.

속성과 인덱스 시그니처를 혼합해서 사용하는 일반적인 타입 시스템 기법 중 하나는 인덱스 시그니처의 원시 속성보다 명명된 속성에 대해 더 구체적인 속성 타입 리터럴을 사용하는것

// ChapterStarts를 사용하는 모든 객체의 preface 속성은 반드시 0이어야 한다. 
interface ChapterStarts {
  preface: 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'.
};

숫자 인덱스 시그니처

  • 객체의 키로 숫자만 허용하는 것이 좋은 상황도 있다.
  • 인덱스 시그니처의 키로 string 대신 number 타입을 사용할 수 있지만, 명명된 속성은 그 타입을 포괄적인 용도의 string 인덱스 시그니처의 타입으로 할당할 수 있어야 한다.
interface MoreNarrowNumbers {
    [i: number]: string;
    [i: string]: string | undefined;
}

const mixesNumbersAndString: 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]: string;
}

중첩 인터페이스

인터페이스 타입도 자체 인터페이스 타입 또는 객체 타입을 속성으로 가질 수 있다.

interface Novel {
  author: {
    name: string;
  };
  setting: Setting;
}

interface Setting {
  place: string;
  year: number;
}

let myNovel: Novel;

// OK
myNovel = {
  author: {
    name: "Jane Austen",
  },
  setting: {
    place: "England",
    year: 1812,
  },
};

myNovel = {
  author: {
    name: "Jane Austen",
  },
  setting: {
    // Error: Property 'place' is missing in type 
    // '{ year: number; }' but required in type 'Setting'.
    year: 1812,
  },
};


인터페이스 확장

extends 키워드를 사용해서 인터페이스를 확장할 수 있다.

interface Writing {
  title: string;
}

interface Novella extends Writing {
  pages: number;
}

// OK
let myNovella: Novella = {
  pages: 190,
  title: "Ethan Frome",
};

let missingPages: Novella = {
  // Error: Property 'pages' is missing in type 
  // '{ title: string; }' but required in type 'Novella'.
  title: "Ethan Frome",
};

재정의된 속성

파생 인터페이스는 다른 타입으로 속성을 다시 선언해 기본 인터페이스의 속성을 재정의하거나 대체할 수 있다.

interface A {
  name: string | null | undefined;
}

interface B extends A {
  name: string;
}

// Error: Interface 'C' incorrectly extends interface 'A'.
interface C extends A {
  name: number | string;
}
// OK
interface C extends A {
  name: null | string;
}

다중 인터페이스 확장

  • 인터페이스는 여러 개의 다른 인터페이스를 확장해서 선언할 수 있다.
  • extends 키워드 위에 쉼표로 인터페이스 이름을 구분해서 사용한다.
  • 파생된 인터페이스는 기본 인터페이스의 모든 멤버를 받는다.
interface GivesNumber {
  giveNumber(): number;
}

interface GivesString {
  giveString(): string;
}

interface GivesBothAndEither extends GivesNumber, GivesString {
  giveEither(): number | string;
}

function useGivesBoth(instance: GivesBothAndEither) {
  instance.giveEither(); // type: string | number
  instance.giveNumber(); // type: number
  instance.giveString(); // type: string
}


인터페이스 병합

  • 동일한 이름의 인터페이스들이 동일한 스코프에 선언되면 인터페이스는 병합된다.
  • 인터페이스가 여러 곳에서 선언되면 코드를 이해하기 어려워 지므로 가능하면 인터페이스 병합은 사용하지 않는 것이 좋다.
  • 그러나 외부 패키지 또는 내장된 전역 인터페이스를 보강할때는 유용하다.
interface Merged {
  fromFirst: string;
}

interface Merged {
  fromSecond: number;
}

// 다음과 같음: 
interface Merged {
  fromFirst: string;
  fromSecond: number;
}

이름이 충돌되는 멤버

  • 병합된 인터페이스는 타입이 다른 동일한 이름의 속성을 여러 번 선언할 수 없다.
  • 속성이 인터페이스에 선언되어 있다면 나중에 병합된 인터페이스에서도 동일한 타입을 사용해야 한다.
interface MergedProperties {
  same: (input: boolean) => string;
  different: (input: string) => string;
}

// same은 속성이 모두 동일해서 문제가 없다. 
// different는 input의 타입이 서로 다르기 때문에 오류가 발생한다. 
interface MergedProperties {
  same: (input: boolean) => string; // OK
  different: (input: number) => string; // Error: Subsequent property declarations must have the same type.
}

  • 하지만 병합된 인터페이스는 동일한 이름과 다른 시그니처를 가진 메서드는 정의할 수 있다.
    👉 메서드에 대한 함수 오버로드 발생
interface MergedMethods {
  different(input: string): string;
}

interface MergedMethods {
  different(input: number): string; // OK
}

0개의 댓글