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'
}
valueLater = 'Emily Dickinson';
valueLater = {
born: true,
name: 'Sappho'
};
- interface와 type alias 사이에는 몇 가지 주요한 차이점이 있습니다.
- interface는 속성 증가를 위해 병합(merge)할 수 있습니다.
- interface는 클래스가 선언된 구조의 타입을 확인하는데 사용할 수 있지만 type alias는 사용할 수 없습니다.
- 일반적으로 interface에서 TypeScript 타입 검사기가 더 빨리 작동합니다.
- 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) {
console.log(page.text);
page.text += '!';
}
- 가변(쓰기 가능한) 속성은 readonly 속성이 필요한 모든 위치에서 읽을 수 있습니다.
const pageIsh = {
text: 'Hello, world!'
}
pageIsh.text += '!';
read(pageIsh);
- readonly는 타입 시스템에만 존재합니다.
- readonly는 단지 TypeScript의 타입 검사기를 사용해 개발 중에 그 속성이 수정되지 못하도록 보호하는 역할을 합니다.
7.2.3 Functions and Methods
- TypeScript는 interface 멤버를 함수로 선언하는 2 가지 방법을 제공합니다.
- 메서드 구문 : interface 멤버를
member(): void
와 같이 객체의 멤버로 호출되는 함수로 선언
- 속성 구문: interface 멤버를
memboer: () => void
와 같이 독립 함수와 동일하게 선언
interface HasBothFunctionTypes {
property: () => string;
method(): string;
}
const hasBoth: HasBothFunctionTypes = {
property: () => '',
method() {
return '';
}
}
hasBoth.property();
hasBoth.method();
- 두 가지 방법 모두 멤버에 선택적 속성 키워드 ?를 추가하여 선택적 속성으로 나타낼 수 있습니다.
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');
}
hasCllCount = doesNotHaveCount;
7.2.5 Index Signatures
- TypeScript는 인덱스 시그니처(index signature) 구문을 제공해 interface가 임의의 키를 받고 해당 키 아래의 특정 타입을 반환할 수 있음을 나타냅니다.
- 인덱스 시그니처는 일반 속성 정의와 유사하지만 키 다음에 타입이 있고
{[i: string]: ...}
와 같이 대괄호를 갖습니다.
interface WordCounts {
[i: string]: number;
}
const counts: WordCounts = {};
counts.apple = 0;
counts.banana = 1;
counts.cherry = false;
- 인덱스 시그니처는 객체에 값을 할당할 때 편리하지만, 타입 안전성을 완벽하게 보장하지는 않습니다.
- 인덱스 시그니처는 객체가 어떤 속성에 접근하든 간에 값을 반환해야 함을 나타내기 때문입니다.
interface DatesByName = {
[i: string]: Date;
}
const publishDates: DatesByName = {
Frankenstein: new Date('1 January 1818');
};
publishDates.Frankenstein;
console.log(publishDates.Frankenstein.toString());
publishDates.Beloved;
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 = {
Outlander: 1991,
}
- 명명된 속성에 대해 더 구체적인 속성 타입 리터럴과 인덱스 시그니처를 혼합해서 사용할 수 있습니다.
- 명명된 속성의 타입이 인덱스 시그니처에 할당될 수 있는 경우, TypeScript는 더 구체적인 속성 타입 리터럴을 사용하는 것을 허용합니다.
interface ChapterStarts {
preface: 0;
[i: string]: number;
}
const correctPreface: ChapterStarts = {
preface: 0,
night: 1,
shopping: 5,
};
const wrongPreface: ChapterStarts = {
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 {
[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: {
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 = {
title: 'The Awaking',
};
let extraProprety: Novella = {
pages: 300,
title: 'baseline',
style: 'Naturalism'
};
7.3.1 Overridden Properties
- 파생 interface는 다른 타입으로 속성을 다시 선언해 기본 interface 속성을 재정의하거나 대체할 수 있습니다.
interface withNullableName {
name: string | null;
}
interface withNonNullableName extends withNullableName {
name: string;
}
interface withNumericName extends withNullableName {
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();
instance.giveNumber();
instance.giveString();
}
7.4 Interface Merging
- interface의 중요한 특징 중 하나가 서로 병합하는 능력입니다.
- 두 개의 interface가 동일한 이름으로 동일한 스코프에 선언된 경우, 선언된 모든 필드를 포함하는 더 큰 interface가 코드에 추가됩니다.
interface Merged {
fromFirst: string;
}
interface Merged {
fromSecond: number;
}
- interface가 여러 곳에서 선언되면 코드를 이해하기 어려워지므로 가능하면 interface 병합을 사용하지 않는 것이 좋습니다.
- interface 병합은 외부 패키지 또는 window 같은 내장 전역 interface를 보강할 때 특히 유용합니다.
interface Window {
myEnvironmentVariable: string;
}
window.myEnvironmentVariable;
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;
different: (input: number) => string;
}
- 병합된 interface는 동일한 이름으로 다른 시그니처를 가진 메서드를 정의할 수 있습니다.
- 이렇게 할 경우, 메서드에 대한 함수 오버로드가 발생합니다.
interface MergedMethods {
different(input: string): string;
}
interface MergedMethods {
different(input: number): string;
}
const methods : MergedMethods = {
different(arg) {
return `${arg}`;
}
}
methods.different('hello');
methods.different(1111);
참고