TypeScript - Learning TypeScript chp7. Interface

이소라·2023년 4월 7일
0

TypeScript

목록 보기
21/28

7. Interface

  • interface는 연관된 이름으로 객체 형태를 설명하는 또 다른 방법입니다.
  • interface는 별칭으로 된 객체 타입과 유사하지만 일반적으로 더 읽기 쉬운 오류 메세지, 더 빠른 컴파일러 성능, 클래스와의 더 나은 상호 운용성을 위해 선호됩니다.

7.1 Type alias vs Interface

  • 객체를 type alias으로 구현하는 구문과 interfcae로 구현하는 구문은 거의 같습니다.
type Poet = {
  born: number;
  name: string;
}

interface Poet {
  born: number;
  name: string;
}
  • interface에 대한 TypeScript의 할당 가능성 검사와 오류 메세지는 객체 타입에서 실행되는 것과 거의 동일합니다.
let valueLater: Poet;

valueLater = {
  born: 1935,
  name: 'Sara Teasdale'
}
// Error: Type 'string' is not assignable to 'Poet'
valueLater = 'Emily Dickinson';

valueLater = {
  // Error: Type 'boolean' is not assignable to type 'number'
  born: true,
  name: 'Sappho'
};
  • interface와 type alias 사이에는 몇 가지 주요한 차이점이 있습니다.
    1. interface는 속성 증가를 위해 병합(merge)할 수 있습니다.
    2. interface는 클래스가 선언된 구조의 타입을 확인하는데 사용할 수 있지만 type alias는 사용할 수 없습니다.
    3. 일반적으로 interface에서 TypeScript 타입 검사기가 더 빨리 작동합니다.
    4. interface는 이름 없는 객체 리터럴의 alias가 아닌 이름 있는 객체로 간주되므로 어려운 특이 케이스에서 나타나는 오류 메세지를 좀 더 쉽게 읽을 수 있습니다.



7.2 Types of Properties

7.2.1 Optional Properties

  • interface의 속성 type annotation : 앞에 ?를 사용하여 선택적 속성임을 나타낼 수 있습니다.
    • interface의 선택적 속성은 제공되거나 생략할 수 있습니다.
interface Book {
  author?: string;
  pages: number;
}

const ok: Book = {
  author: 'Rita Dove',
  pages: 80,
};

const missing: Book = {
  pages: 80
}

7.2.2 Read-Only Properties

  • TypeScript는 속성 이름 앞에 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 += '!';
}
  • 가변(쓰기 가능한) 속성은 readonly 속성이 필요한 모든 위치에서 읽을 수 있습니다.
const pageIsh = {
  text: 'Hello, world!'
}
// Ok, pageIsh는 Page 객체가 아니라 text가 있는 유추된 객체 타입
pageIsh.text += '!';
// Ok, pageIsh의 더 구체적인 버전인 Page를 읽습니다.
read(pageIsh);
  • readonly는 타입 시스템에만 존재합니다.
    • readonly는 단지 TypeScript의 타입 검사기를 사용해 개발 중에 그 속성이 수정되지 못하도록 보호하는 역할을 합니다.

7.2.3 Functions and Methods

  • TypeScript는 interface 멤버를 함수로 선언하는 2 가지 방법을 제공합니다.
    1. 메서드 구문 : interface 멤버를 member(): void와 같이 객체의 멤버로 호출되는 함수로 선언
    2. 속성 구문: interface 멤버를 memboer: () => void와 같이 독립 함수와 동일하게 선언
interface HasBothFunctionTypes {
  property: () => string;
  method(): string;
}

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

hasBoth.property(); // OK
hasBoth.method(); // OK
  • 두 가지 방법 모두 멤버에 선택적 속성 키워드 ?를 추가하여 선택적 속성으로 나타낼 수 있습니다.
interface HasBothFunctionTypes {
  property?: () => string;
  method?(): string;
}
  • 메서드와 속성의 주요 차이점은 다음과 같습니다.
    • 메서드는 readonly로 선언할 수 없지만 속성은 가능합니다.
    • interface 병합은 메서드와 속성을 다르게 처리합니다.

7.2.4 Call Signatures

  • interface와 객체 타입은 호출 시그니처(call signature)로 선언할 수 있습니다.
    • 호출 시그니처는 함수처럼 호출되는 방식에 대한 타입 시스템의 설명입니다.
    • 호출 시그니처가 선언한 방식으로 호출되는 값만 할당할 수 있습니다.
    • 호출 시그니처는 함수 타입과는 비슷하지만 화살표(=>) 대신 콜론(:)으로 표시합니다.
type FunctionAlias = (input: string) => number;

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

const typedFunctionAlias: FunctionAlias = (input) => input.length;
  • 호출 시그니처는 사용자 정의 속성을 추가로 갖는 함수를 설명하는 데 사용할 수 있습니다.
inteface 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;

function doesNotHaveCount() {
  console.log('No idea');
}

// Error: Property 'count' is missing type '() => void' but required in type 'FunctionWithCalls'
hasCllCount = doesNotHaveCount;

7.2.5 Index Signatures

  • TypeScript는 인덱스 시그니처(index signature) 구문을 제공해 interface가 임의의 키를 받고 해당 키 아래의 특정 타입을 반환할 수 있음을 나타냅니다.
    • 인덱스 시그니처는 일반 속성 정의와 유사하지만 키 다음에 타입이 있고 {[i: string]: ...}와 같이 대괄호를 갖습니다.
interface WordCounts {
  [i: string]: number;
}

const counts: WordCounts = {};
// Ok
counts.apple = 0;
// Ok
counts.banana = 1;
// Error: Type 'boolean' is not assignable to type 'number'
counts.cherry = false;
  • 인덱스 시그니처는 객체에 값을 할당할 때 편리하지만, 타입 안전성을 완벽하게 보장하지는 않습니다.
    • 인덱스 시그니처는 객체가 어떤 속성에 접근하든 간에 값을 반환해야 함을 나타내기 때문입니다.
interface DatesByName = {
  [i: string]: Date;
}

const publishDates: DatesByName = {
  Frankenstein: new Date('1 January 1818');
};
// (property) Frankenstein: Date
publishDates.Frankenstein;
// Ok
console.log(publishDates.Frankenstein.toString());

// (property) Beloved: Date
publishDates.Beloved; // 런타임 값은 undefined
// Runtime error: Cannot read property 'toString' of undefined
console.log(publishDates.Frankenstein.toString());
  • 키/값 쌍을 저장하려고 하는데 키를 미리 알 수 없다면 Map을 사용하는 편이 더 안전합니다.
    • Map의 get 메서드는 항상 키가 존재하지 않음을 나타내기 위해서 | undefined 타입을 반환합니다.

index siganature with property

  • interface는 명시적으로 명명된 속성과 포괄적인 용도의 인덱스 시그니처를 한 번에 포함할 수 있습니다.
    • 각각의 명명된 속성의 타입은 포괄적인 용도의 인덱스 시그니처로 할당할 수 있어야 합니다.
    • 명명된 속성이 더 구체적인 타입을 제공하고, 다른 모든 속성은 인덱스 시그니처의 타입으로 대체합니다.
interface HistoricalNovels {
  Oroonoko: number;
  [i: string]: 
}

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

const missingOroonoko: HistoricalNovels = {
  // Error: Property 'Oroonoko' is missing in type { Outlander: 1991}
  //	but required in type 'HistoricalNovels'
  Outlander: 1991,
}
  • 명명된 속성에 대해 더 구체적인 속성 타입 리터럴과 인덱스 시그니처를 혼합해서 사용할 수 있습니다.
    • 명명된 속성의 타입이 인덱스 시그니처에 할당될 수 있는 경우, TypeScript는 더 구체적인 속성 타입 리터럴을 사용하는 것을 허용합니다.
interface ChapterStarts {
  preface: 0;
  [i: string]: number;
}

const correctPreface: ChapterStarts = {
  preface: 0,
  night: 1,
  shopping: 5,
};

const wrongPreface: ChapterStarts = {
  // Error: Type '1' is not assignable to type '0'
  preface: 1,
};

number index signature

  • 인덱스 시그니처 속성은 string, number, symbol, 이러한 타입들의 union 그리고 템플릿 리터럴만 허용합니다.
  • 인덱스 시그니처 속성으로 string이 아닌 타입을 사용할 때 반환값은 string 인덱스 속성의 반환값의 subtype이어야 합니다.
    • JavaScript에서 string이 아닌 인덱스는 실제로 문자열로 변환되기 때문입니다.
interface MoreNarrowNumbers {
  [i: number]: string;
  [i: string]: string | undefined;
}

const mixesNumbersAndString: MoreNarrowNumbers = {
  0: '',
  key1: '',
  key2: undefined
};

interface MoreNarrowStrings {
  // Error: 'number' index type 'string | undefined'
  // is not assignable to 'string' index type 'string'
  [i: number]: string | undefined;
  [i: string]: string;
}

7.2.6 Nested Interfaces

  • interface도 자체 interface 타입 혹은 객체 타입을 속성으로 가질 수 있습니다.
interface Novel {
  author: {
    name: string;
  }
  setting: Setting;
}

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

let myNovel: Novel;

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

myNovel = {
  author: {
    name: 'Emily Bronte',
  },
  setting: {
    // Error: Property 'year' is missing in type { place: string; } bute required in type 'Setting'
    place: 'West Yorkshire',
  }
};



7.3 Interface Extensions

  • TypeScript는 interface가 다른 interface의 모든 멤버를 복사해서 선언할 수 있는 확장된 interface를 허용합니다.
    • interface 이름 뒤에 extends 키워드와 확장하고자 하는 기본 interface를 추가하여 확장된 파생 interface라는 것을 표시합니다.
    • 파생 interface를 준수하는 모든 객체가 기본 interface의 모든 멤버를 가져야 합니다.
interface Writing {
  title: string;
}

interface Novella extends Writing {
  pages: number;
}

let myNovella: Novella = {
  pages: 195,
  title: 'Ethan Frome',
};

let missingPages: Novella = {
  // Error: Property 'pages' is missing in type '{ title: string }' but required in type 'Novella'
  title: 'The Awaking',
};

let extraProprety: Novella = {
  pages: 300,
  title: 'baseline',
  // Error: Property 'style' does not exist in type 'Novella'
  style: 'Naturalism'
};

7.3.1 Overridden Properties

  • 파생 interface는 다른 타입으로 속성을 다시 선언해 기본 interface 속성을 재정의하거나 대체할 수 있습니다.
interface withNullableName {
  name: string | null;
}

interface withNonNullableName extends withNullableName {
  name: string;
}

interface withNumericName extends withNullableName {
  // Error: Type 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;
}

7.3.2 Extending Multiple Interfaces

  • TypeScript의 interface는 여러 개의 다른 interface를 확장하여 선언할 수 있습니다.
    • 파생 interface 이름에 있는 extends 키워드 뒤에 쉼표로 interface를 구분하여 여러 개의 기본 interface를 명시할 수 있습니다.
    • 파생 interface는 모든 기본 interface의 모든 멤버를 받습니다.
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 Interface Merging

  • interface의 중요한 특징 중 하나가 서로 병합하는 능력입니다.
    • 두 개의 interface가 동일한 이름으로 동일한 스코프에 선언된 경우, 선언된 모든 필드를 포함하는 더 큰 interface가 코드에 추가됩니다.
interface Merged {
  fromFirst: string;
}

interface Merged {
  fromSecond: number;
}

/*
다음과 같음
interface Merge {
  fromFirst: string;
  fromSecond: number;
}
*/
  • interface가 여러 곳에서 선언되면 코드를 이해하기 어려워지므로 가능하면 interface 병합을 사용하지 않는 것이 좋습니다.
  • interface 병합은 외부 패키지 또는 window 같은 내장 전역 interface를 보강할 때 특히 유용합니다.
// window의 interface에 병합됨
interface Window {
  myEnvironmentVariable: string;
}

window.myEnvironmentVariable; // string;

7.4.1 Member Naming Conflicts

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

interface MergedProperties {
  same: (input: boolean) => string;
  // Error: Subsequent property declarations must have the same type.
  //	Property 'different' must be the type '(input: string) => string;'
  //		but here has type '(input: number) => string'
  different: (input: number) => string;
}
  • 병합된 interface는 동일한 이름으로 다른 시그니처를 가진 메서드를 정의할 수 있습니다.
    • 이렇게 할 경우, 메서드에 대한 함수 오버로드가 발생합니다.
interface MergedMethods {
  different(input: string): string;
}

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

const methods : MergedMethods = {
  different(arg) {
    return `${arg}`;
  }
}
//  MergedMethods.different(input: string): string (+1 overload)
methods.different('hello');
// MergedMethods.different(input: number): string (+1 overload)
methods.different(1111);



참고

0개의 댓글