타입스크립트 인터페이스
인터페이스와 타입 별칭 사이에는 몇 가지 차이점이 있다.
- 인터페이스는 속성 증가를 위해 merge할 수 있다. 이 기능은 내장된 전역 인터페이스 또는 npm 패키지와 같은 외부 코드를 사용할 때 특히 유용하다.
- 인터페이스는 클래스 구조의 타입을 확인하는 데 사용할 수 있지만 타입 별칭은 그 용도로 사용할 수 없다.
- 인터페이스는 이름 없는 객체 리터럴의 별칭이 아닌, '이름 있는 객체'로 간주되므로 특이한 오류 케이스를 좀 더 쉽게 읽을 수 있다.
속성 타입
선택적 속성
객체 타입과 마찬가지로 모든 객체가 필수적으로 인터페이스 속성을 가질 필요는 없다. 타입 애너테이션 :
앞에 ?
를 사용해 인터페이스의 속성이 선택적 속성임을 나타낼 수 있다.
interface Book {
author?: string;
pages: number;
};
const ok: Book = {
author: "무라카미 하루키",
pages: 300,
};
const missing: Book = {
pages: 80,
};
읽기 전용 속성
타입스크립트는 속성 이름 앞에 readonly
키워드를 추가해 다른 값으로 설정될 수 없음을 나타낼 수 있다. 이러한 readonly
속성은 평소대로 읽을 수 있지만 새로운 값으로 재할당하지 못한다.
interface Page {
readonly text: string;
}
function read(page: Page) {
// Ok: text 속성을 수정하지 않고 읽는 것
console.log(page.text);
page.text += "!";
// ~~~~
// Error: Cannot assign to 'text' because it is a read-only property.
}
readonly
제한자는 타입 시스템에만 존재하며 인터페이스에서만 사용할 수 있다. readonly
제한자는 객체의 인터페이스를 선언하는 위치에서만 사용되고 실제 객체에는 적용되지 않는다.
interface Page {
readonly text: string;
}
function read(page: Page) {
console.log(page.text);
// page.text += "!";
}
const pageIsh = {
text: "Hello, world!",
};
// ㅇㅋ: pageIsh는 Page 객체가 아니라 text가 있는, 유추된 객체 타입이다.
pageIsh.text += "!";
// ㅇㅋ: pageIsh의 더 구체적인 버전인 Page를 읽는다.
read(pageIsh);
명시적 타입 애너테이션인 : Page
로 변수 pageIsh
를 선언하면 타입스크립트에 text 속성이 readonly라고 가리키게 된다.
const pageIsh: Page = {
text: "Hello, world!",
};
pageIsh.text += "!!!";
//~~~~~~~~~~~
// Error
함수와 메서드
타입스크립트는 인터페이스 멤버를 함수로 선언하는 두 가지 방법을 제공한다.
- 메서드 구문: 인터페이스 멤버를
member(): void
와 같이 객체의 멤버로서 호출되는 함수로 선언 - 속성 구문: 인터페이스의 멤버를
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
불가능, 프로퍼티로 선언할 때는 가능
호출 시그니처(call signature)
호출 시그니처는 값을 함수처럼 호출하는 방식에 대한 타입 시스템의 설명이다.
호출 시그니처가 선언한 방식으로 호출되는 값만 인터페이스에 할당할 수 있다. 즉, 할당 가능한 매개변수와 반환 타입을 가진 함수다.
type FunctionAlias = (input: string) => number;
interface CallSignature {
(input: string): number;
}
// 타입: (input: string) => number
const typedFunctionAlias: FunctionAlias = (input) => input.length; // ㅇㅋ
// 타입: (input: string) => number
const typedCallSignature: CallSignature = (input) => input.length; // ㅇㅋ
호출 시그니처는 사용자 정의 프로퍼티를 추가로 갖는 함수를 설명하는 데 사용할 수 있다. 타입스크립트는 함수 선언에 추가된 프로퍼티를 해당 함수 선언의 타입에 추가하는 것으로 인식한다.
interface FunctionWithCount {
count: number;
(): void; // call signature
}
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!");
}
hasCallCount = doesNotHaveCount;
//~~~~~~~~~~
// Error: Property 'count' is missing in type
// '() => void' but required in type 'FunctionWith'
인덱스 시그니처
객체의 값을 불러올 때 obj[prop]
를 사용할 수 있다. 인덱스 시그니처는 그 느낌과 비슷하다.
타입스크립트는 인덱스 시그니처 구문을 제공해 인터페이스의 객체가 임의의 키를 받고, 해당 키 아래의 특정 타입을 반환할 수 있음을 나타낸다.
interface WordCounts {
[i: string]: number;
}
const counts: WordCounts = {};
counts.apple = 0;
counts.banana = 1;
counts.cherry = false;
//~~~~~~~~~~~
// Error: Type 'boolean' is not assignable to type 'nubmer'.
인터페이스 확장
타입스크립트는 확장된 인터페이스를 허용한다.
확장된 인터페이스란 한 인터페이스가 다른 인터페이스의 모든 멤버를 복사해서 선언할 수 있는 것을 의미한다.
확장할 인터페이스의 이름 뒤에 extends
키워드를 추가해서 다른 인터페이스를 확장한 인터페이스라는 것을 표시한다.
이렇게 하면 파생 인터페이스를 준수하는 모든 객체가 기본 인터페이스의 모든 멤버 또한 가져야 한다는 것을 타입스크립트에 알려준다.
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 Awakening",
}
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: "Naturalism",
}
인터페이스 확장은 프로젝트의 한 entity 타입이 다른 entity의 모든 멤버를 포함하는 상위 집합을 나타낼 수 있는 실용적인 방법이다.
인터페이스 확장 덕분에 여러 인터페이스의 관계를 나타내기 위해 동일한 코드를 반복 입력하는 작업을 피할 수 있게 되었다.
재정의된 속성(override)
기본 인터페이스 속성의 타입은 파생 인터페이스 속성의 타입을 포함해야 한다.
파생 인터페이스는 다른 타입으로 속성을 다시 선언해 기본 인터페이스의 속성을 재정의하거나 대체할 수 있다. 타입스크립트의 타입 검사기는 재정의된 속성이 기본 속성에 할당되어 있도록 강요한다. 이렇게 하면 파생 인터페이스 타입의 인스턴스를 기본 인터페이스 타입에 할당할 수 있다.
속성을 재선언하는 대부분의 파생 인터페이스는 해당 속성을 유니언 타입의 더 구체적인 하위 집합으로 만들거나 속성을 기본 인터페이스의 타입에서 확장된 타입으로 만들기 위해 사용한다.
interface StringWithNull {
name: string | null;
}
interface OnlyString extends StringWithNull{
name: string;
}
interface NumWithString extends StringWithNull{
// ~~~~~~~~~~~
// Error: Interface 'NumWithString' incorrectly extends interface
// 'StringWithNull'.
// Types of property 'name' are incompatible.
// Type 'number | string' is not assignable to type 'string | null'.
// Type 'number' is not assignable to type 'string'.
name: number | string;
}
interface OnlyBool {
name: boolean;
}
interface BoolWithNum extends OnlyBool{
// ~~~~~~~~~~~
// Error: Interface 'BollWithNum' incorrectly extends interface
// 'OnlyBool'.
// Types of property 'name' are incompatible.
// Type 'boolean | number' is not assignable to type 'boolean'.
// Type 'number' is not assignable to type 'boolean'.
name: boolean | number;
}
다중 인터페이스 확장
타입스크립트의 인터페이스는 여러 개의 다른 인터페이스를 확장해서 선언할 수 있다. 파생 인터페이스 이름에 있는 extends
키워드 뒤에 쉼표로 인터페이스 이름을 구분해 사용하면 된다.
interface GivesNumber {
giveNumber(): number;
}
interface GivesString {
giveString(): string;
}
interface GivesBothAndEither extends GivesNumber, GivesString {
giveEither(): number | string;
}
function useGivesBothAndEither(instance: GivesBothAndEither) {
instance.giveEither();
instance.giveNumber();
instance.giveString();
}