7. 인터페이스
- 인터페이스(interface): 연관된 이름으로 객체 형태를 설명하는 또 다른 방법
별칭으로 된 객체 타입과 여러 면에서 유사하지만 일반적으로 더 읽기 쉬운 오류 메시지, 더 빠른 컴파일러 성능, 클래스와의 더 나은 상호 운용성을 위해 선호됨
7.1 타입 별칭 vs. 인터페이스
type Poet = { // 타입 별칭
born: number;
name: string;
}
interface Poet = { // 인터페이스
born: number;
name: string;
}
다음과 같이 객체를 타입 별칭으로 구현하는 구문과 인터페이스로 구현한 구문은 거의 동일함
마찬가지로, 인터페이스에 대한 타입스크립트의 할당 가능성 검사와 오류 메시지는 객체 타입에서 실행되는 것과 거의 동일함
- 인터페이스와 타입 별칭 사이의 주요 차이점
① 인터페이스는 속성 증가를 위해 병합할 수 있음 → 내장된 전역 인터페이스 또는 npm 패키지와 같은 외부 코드를 사용할 때 특히 유용
② 인터페이스는 클래스가 선언된 구조 타입을 확인하는데 사용 가능 ↔ 타입 별칭은 사용 불가능
③ 인터페이스에서 타입스크립트 타입 검사기가 더 빨리 작동함 → 타입 별칭이 하는 것처럼 새로운 객체 리터럴의 동적인 복사 붙여넣기보다 내부적으로 더 쉽게 캐시할 수 있는 명명된 타입을 선언함
④ 인터페이스는 어려운 특이 케이스에서 나타나는 오류 메시지를 좀 더 쉽게 읽을 수 있음 (이름 없는 객체 리터럴의 별칭이 아닌 이름 있는 객체로 간주되기 때문)
7.2 속성 타입
- 타입스크립트는 인터페이스가 자바스크립트 객체를 실제로 사용할 때 낯설고 이상할 수 있는 부분을 모델링할 수 있도록 유용한 타입 시스템 도구를 제공함
7.2.1 선택적 속성
- 객체 타입과 마찬가지로 타입 에너테이션: 앞에 ??를 사용해 인터페이스의 속성이 선택적 속성임을 나타냄
interface Book {
author?: string;
pages: number;
};
// Ok
const ok: Book = {
author: "Rita Dove",
pages: 80,
};
const missing: Book = {
pages: 80
};
7.2.2 읽기 전용 속성
- 속성 이름 앞에 readonly 키워드를 추가해 인터페이스에 정의된 객체의 속성을 재할당 하지 못함
interface Page {
readonly text: string;
}
function read(page: Page) {
// Ok : text 속성을 수정하지 않고 읽는 것
console.log(page.text);
page.text += "!"; // Error: readonly 속성에 재할당하려해서 오류 발생
}
readonly 제한자는 타입 시스템에만 존재하며 인터페이스에서만 사용 가능
객체의 인터페이스를 선언하는 위치에서만 사용되고 실제 객체에서는 적용 안됨
const pageIsh = {
text: "Hello, world!",
};
pageIsh.text += "!"; // Ok: pageIsh는 Page 객체가 아니라 text가 있는, 유추된 객체 타입
read(pageIsh); // pageIsh의 더 구체적인 버전의 Page를 읽음
+ readonly는 타입 시스템 구성 요소일 뿐 컴파일된 자바스크립트 출력 코드에는 존재하지 않음 → 단지 타입스크립트 타입 검사기를 사용해 개발 중에 그 속성이 수정되지 못하도록 보호하는 역할을 함
7.2.3 함수와 메서드
- 인터페이스 멤버를 함수로 선언하는 방법
① 메서드 구문: 인터페이스 멤버를 member(): void와 같이 객체의 멤버로 호출되는 함수로 선언
② 속성 구문: 인터페이스의 멤버를 member: () => void와 같이 독립 함수와 동일하게 선언
interface HasBothFuntionTypes {
property: () => string;
method(): string;
}
const hasBoth: HasBothFuntionTypes = {
property: () => "",
method() {
return "";
}
};
hasBoth.property(); // Ok
hasBoth.method(); // Ok
interface OptionalReadonlyFunctions {
optionalProperty?: () => string; // 선택적 속성
optionalMethod?: () => string;
}
- 메소드와 속성의 주요 차이점
① 메서드는 readonly로 선언할 수 없지만 속성은 가능함
② 인터페이스 병합은 메서드와 속성을 다르게 처리함
③ 타입에서 수행되는 일부 작업은 메서드와 속성을 다르게 처리함
7.2.4 호출 시그니처
type FunctionAlias = (input: string) => number;
interface CallSignature {
(input: string): number;
}
const typeFunctionAlias: FunctionAlias = (input) => input.lenth; // 타입: (input: string) => number
const typedCallSignature: CallSignature = (input) => input.length; // 타입: (input: string) => number
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: count 속성이 주어지지 않음
7.2.5 인덱스 시그니처
- 타입스크립트는 인덱스 시그니처 구문을 제공해 인터페이스의 객체가 임의의 키를 받고, 해당 키 아래의 특정 타입을 반환할 수 있음을 나타냄
interface WordCounts {
[i: string]: number;
}
const counts: WordCounts = {};
counts.apple = 0; // Ok
counts.banana = 1; // Ok
counts.cherry = false; // Error: 객체 값이 boolean일 수 없음
- 인덱스 시그니처는 객체에 값을 할당할 때 편리하지만 타입 안정성을 완벽하게 보장하지는 않음 → 객체가 어떤 속성에 접근하든 간에 값을 반환해야 함
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()); // Error: 타입 시스템에서는 오류가 나지 않지만 실제 런타임에서는 오류가 발생함
1) 속성과 인덱스 시그니처 혼합
- 인터페이스는 명시적으로 명명된 속성과 포괄적인 용도의 string 인덱스 시그니처를 한번에 포함할 수 있음
① 각각의 명명된 속성의 타입은 포괄적인 용도의 인덱스 시그니처로 할당할 수 있어야 함
② 명명된 속성이 더 구체적인 타입을 제공하고, 다른 모든 속성은 인덱스 시그니처의 타입으로 대체하는 것으로 혼합해서 사용할 수 있음
interface HistoricalNovels {
Oroonoko: number;
[i: string]: number;
}
const novels: HistoricalNovels = { // Ok
Outlander: 1991,
Oroonoko: 1688,
}
const missingOroonoko: HistoricalNovels = {
Outlander: 1991, // Error: Oroonoko 타입 없음
}
- 인덱스 시그니처의 원시 속성보다 명명된 속성에 대해 더 구체적인 속성 타입 리터럴을 사용할 수 있음
interface ChapterStarts {
preface: 0;
[i: string]: number;
}
const correctPreface: ChapterStarts = {
preface: 0,
night: 1,
shopping: 5
}
const wrongPreface: ChapterStarts = {
preface: 1, // Error: preface 속성은 반드시 0이어야 함
}
2) 숫자 인덱스 시그니처
- 인터페이스는 명시적으로 명명된 속성과 포괄적인 용도의 string 인덱스 시그니처를 한번에 포함할 수 있음
interface MoreNarrowNumbers {
[i: number]: string;
[i: string]: string | undefined;
}
const mixesNumbersAndStrings: MoreNarrowNumbers = { // Ok
0: '',
key1: '',
key2: undefined,
}
interface MoreNarrowStrings {
[i: number]: string | undefined; // Error: ???
[i: string]: string;
}
7.2.6 중첩 인터페이스
- 인터페이스 타입도 자체 인터페이스 타입 혹은 객체 타입을 속성으로 가질 수 있음
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', // Error: 타입에 year 없음
}
};
7.3 인터페이스 확장
- 타입스크립트는 인터페이스가 다른 인터페이스의 모든 멤버를 복사해서 선언할 수 있는 확장된 인터페이스를 허용함 ⇒ 확장할 인터페이스의 이름 뒤에 extends 키워드를 추가
interface Writing {
title: string;
}
interface Novella extends Writing {
pages: number;
}
let myNovella: Novella = { // Ok
pages: 195,
title: "Ethan Frome",
};
let missingPages: Novella = {
title: "The Awakening", // Error: pages 멤버 없음
};
let extraProperty: Novella = {
pages: 300, // Error: title 멤버 없음
strategy: "baseline",
style: "Naturalism",
};
7.3.1 재정의된 속성
- 파생 인터페이스는 다른 타입으로 속성을 다시 선언해 기본 인터페이스의 속성을 재정의하거나 대체할 수 있음
→ 해당 속성을 유니언 타입의 더 구체적인 하위 집합으로 만들거나 속성을 기본 인터페이스의 타입에서 확장된 타입으로 만들기 위해 사용함
interface WithNullableName {
name: string | null;
}
interface WithNonNullableName extends WithNullableName {
name: string;
}
interface WithNumbericName extends WithNullableName {
name: number | string; // Error: number | string은 string | null에 할당할 수 없음
}
7.3.2 다중 인터페이스 확장
- 파생 인터페이스 이름에 있는 extends 키워드 뒤에 쉼표로 인터페이스 이름을 구분해 사용하면 여러개의 다른 인터페이스를 확장해서 선언할 수 있음
interface GivesNumber {
giveNumber(): number;
}
interface GiveString {
giveString(): string;
}
interface GivesBothAndEither extends GivesNumber, GiveString {
instance.giveEither(); // 타입: nubmer | string
instance.giveNumber(); // 타입: nubmer
instance.giveString(); // 타입: string
}
7.4 인터페이스 병합
- 타입스크립트는 인터페이스가 자바스크립트 객체를 실제로 사용할 때 낯설고 이상할 수 있는 부분을 모델링할 수 있도록 유용한 타입 시스템 도구를 제공함
interface Merged {
fromFirst: string;
}
interface Merged {
fromSecond: number;
}
// ⇒ interface Merged {
// fromFirst: string;
// fromSecond: number;
// }
인터페이스가 여러곳에 선언되면 코드를 이해하기 어려워지므로 가능한 인터페이스 병합을 사용하지 않는 것이 좋음
⇒ but, 외부 패키지 또는 Window 같은 내장된 전역 인터페이스를 보강하는데 특히 유용함
interface Window {
myEnvironmentVariable: string;
}
window.myEnvironmentVariable; // 타입: string
7.4.1 이름이 충돌되는 멤버
- 속성이 이미 인터페이스에 선언되어 있는 경우, 나중에 병합된 인터페이스에서도 동일한 타입을 사용해야 함
interface MergedProperties {
same: (input: boolean) => string;
different: (input: string) => string;
}
interface MergedProperties {
same: (input: boolean) => string; // Ok
different: (input: number) => string; // Error: 위에 선언된 MergedProperties.different의 속성과 다르므로 에러 발생
}
- 병합된 인터페이스는 동일한 이름과 다른 시그니처를 가진 메서드는 정의할 수 있음 → 메서드에 대한 함수 오버로드 발생
interface MergedMethods {
different(input: string): string;
}
interface MergedMethods {
different(input: number): string; // Ok
}
'TypeScript > 러닝 타입스크립트' 카테고리의 다른 글
[CHAPTER 6] 배열 (0) | 2023.05.07 |
---|---|
[CHAPTER 5] 함수 (0) | 2023.05.01 |
[CHAPTER 4] 객체 (0) | 2023.04.19 |
[CHAPTER 3] 유니언과 리터럴 (0) | 2023.04.19 |
[CHAPTER 2] 타입 시스템 (0) | 2023.04.10 |