2023-04-12 · 21 min read · 학습
러닝 타입스크립트 CHAPTER 7 - 인터페이스
CHAPTER 7 인터페이스
인터페이스는 연관된 이름으로 객체 형태를 설명하는 또 다른 방법입니다. 별칭으로 된 객체 타입과 여러 면에서 유사하지만 일반적으로 더 읽기 쉬운 오류 메시지, 더 빠른 컴파일러 성능, 클래스와의 더 나은 상호 운용성을 위해 선호됩니다.
타입 별칭 vs 인터페이스
타입 별칭과 인터페이스로 구현한 다음 두 구문은 거의 같습니다.
type Poet = { born: number name: string}
interface Poet { born: number name: string}그러나 인터페이스와 타입 별칭 사이에는 몇 가지 주요 차이점이 있습니다.
- 인터페이스는 속성 증가를 위해 병합할 수 있습니다. 이 기능은 내장된 전역 인터페이스 또는 npm 패키지와 같은 외부 코드를 사용할 때 특히 유용합니다.
- 인터페이스는 클래스가 선언된 구조의 타입을 확인하는 데 사용할 수 있지만 타입 별칭은 사용할 수 없습니다.
- 일반적으로 인터페이스에서 타입스크립트 타입 검사기가 더 빨리 작동합니다. 인터페이스는 타입 별칭이 하는 것처럼 새로운 객체 리터럴의 동적인 복사 붙여넣기보다 내부적으로 더 쉽게 캐시할 수 있는 명명된 타입을 선언합니다.
- 인터페이스는 이름 없는 객체 리터럴의 별칭이 아닌 이름 있는 (명명된) 객체로 간주되므로 오류 메시지를 좀 더 쉽게 읽을 수 있습니다.
속성 타입
getter와 setter를 포함해, 가끔 존재하는 속성, 또는 임의의 속성 이름을 사용하는 등 자바스크립트 객체를 실제로 사용할 때 낯설고 이상할 수 있습니다.
이때 인터페이스는 이상한 부분을 모델링할 수 있도록 유용한 타입 시스템 도구를 제공합니다.
선택적 속성
객체 타입과 마찬가지로 모든 객체가 필수적으로 인터페이스 속성을 가질 필요는 없습니다.
타입 애너테이션 : 앞에 ?를 사용해 선택적 속성임을 나타낼 수 있습니다.
interface Book { author?: string pages: number}
// OKconst ok: Book = { author: 'Rita Dove', pages: 80,}
// OKconst missing: Book = { pages: 80,}undefined를 포함한 유니언 타입의 선택적 속성과 일반 속성 사이의 차이점과 관련된 주의 사항은 인터페이스에도 적용됩니다.
읽기 전용 속성
경우에 따라 인터페이스에 정의된 객체의 속성을 재할당하지 못하도록 인터페이스 사용자를 차단하고 싶을 수 있습니다.
타입스크립트는 속성 이름 앞에 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 read-only property.}readonly 제한자는 타입 시스템에만 존재하며 인터페이스에서만 사용할 수있습니다.
readonly 제한자는 객체의 인터페이스를 선언하는 위치에서만 사용되고 실제 객체에는 적용되지 않습니다.
readonly 인터페이스 멤버는 코드 영역에서 객체를 의도치 않게 수정하는 것을 막는 편리한 방법입니다.
그러나 readonly는 타입 시스템 구성 요소일 뿐 컴파일된 자바스크립트 출력 코드에는 존재하지 않습니다.
readonly는 단지 타입스크립트 타입 검사기를 사용해 개발 중에 그 속성이 수정되지 못하도록 보호하는 역할을 합니다.
함수와 메서드
자바스크립트에서 객체 멤버가 객체 함수가 되는 것은 매우 일반적입니다. 타입스크립트는 인터페이스 멤버를 함수로 선언하는 두 가지 방법을 제공합니다.
- 메서드 구문: 인터페이스 멤버를
member(): void와 같이 객체의 멤버로 호출되는 함수로 선언 - 속성 구문: 인터페이스의 멤버를
member: () => void와 같이 독립 함수와 동일하게 선언
두 가지 선언 방법은 자바스크립트에서 객체를 함수로 선언하는 방법과 동일합니다.
interface HasBothFunctionTypes { property: () => string method(): string}
const hasBoth: HasBothFunctionTypes = { property: () => '', method() { return '' },}
hasBoth.property() // OKhasBoth.method() // OK두 가지 방법 모두 앞서 살펴본 선택적 속성 키워드를 사용하여 필수로 제공하지 않아도 되는 멤버로 나타낼 수 있습니다.
메서드와 속성 선언은 대부분 서로 교환하여 사용할 수 있습니다. 메서드와 속성의 주요 차이점은 다음과 같습니다.
- 메서드는
readonly로 선언할 수 없지만 속성은 가능합니다. - 인터페이스 병합은 메서드와 속성을 다르게 처리합니다.
- 타입에서 수행되는 일부 작업은 메서드와 속성을 다르게 처리합니다.
보통 기본 함수가 this를 참조할 수 있다는 것을 알고 있다면 메서드 함수를 사용하는 것을 추천합니다.
가장 일반적으로 클래스의 인스턴스에서 사용됩니다. 반대의 경우에는 속성 함수를 사용합니다.
호출 시그니처
인터페이스와 객체 타입은 호출 시그니처로 선언할 수 있습니다. 호출 시그니처는 값을 함수처럼 호출하는 방식에 대한 타입 시스템의 설명입니다. 호출 시그니처가 선언한 방식으로 호출되는 값만 인터페이스에 할당할 수 있습니다.
type FunctionAlias = (input: string) => number
interface CallSignature { (input: string): number}
// 타입: (input: string) => numberconst typedFunctionAlias: FunctionAlias = (input) => input.length // OK
// 타입: (input: string) => numberconst typedCallSignature: 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} times!`)}
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 'FunctionWithCalls'인덱스 시그니처
일부 자바스크립트 프로젝트는 임의의 string 키에 값을 저장하기 위한 객체를 생성합니다.
이러한 ‘컨테이너’ 객체의 경우 모든 가능한 키에 대해 필드가 있는 인터페이스를 선언하는 것은 비효율적입니다.
이때 타입스크립트는 인덱스 시그니처 구문을 제공해 인터페이스의 객체가 임의의 키를 받고, 해당 키 아래의 특정 타입을 반환할 수 있음을 나타냅니다.
인덱스 시그니처는 일반적으로 {[i: string]: ... }와 같은 형태를 지닙니다.
interface WordCounts { [i: string]: number}
const counts: WordCounts = {}
counts.apple = 0 // OKcounts.cherry = 1 // OK
counts.cherry = false// Error: Type 'boolean' is not assignable to type 'number'.인덱스 시그니처는 객체에 값을 할당할 때 편리하지만 타입 안정성을 완벽하게 보장하지는 않습니다.
인덱스 시그니처는 객체가 어떤 속성에 접근하든 간에 값을 반환해야 함을 나타냅니다.
interface DatesByName { [i: string]: Date}
const publishDates: DatesByName { Frankenstein: new Date('1 January 1818')}
publishDates.Frankenstein // 타입 Dateconsole.log(publishDates.Frankenstein.toString()) // OK
publishDates.Beloved // 타입은 Date이지만 런타임 값은 undefinedconsole.log(publishDates.Beloved.toString())// 타입 시스템에서는 오류가 나지 않지만 실제 런타임에서는 오류가 발생// Runtime error: Cannot read property 'toString'// of undefined (reading publishDates.Beloved)키/값 쌍을 저장하려고 하는데 키를 미리 알 수 없다면 Map을 사용하는 편이 더 안전합니다.
.get 메서드는 항상 키가 존재하지 않음을 나타내기 위해 | undefined 타입을 반환합니다.
속성과 인덱스 시그니처 혼합
인터페이스는 명시적으로 명명된 속성과 포괄적인 용도의 string 인덱스 시그니처를 한 번에 포함할 수 있습니다. 각각의 명명된 속성의 타입은 포괄적인 용도의 인덱스 시그니처로 할당할 수 있어야 합니다. 명명된 속성이 더 구체적인 타입을 제공하고, 다른 모든 속성은 인덱스 시그니처의 타입으로 대체하는 것으로 혼합해서 사용할 수 있습니다.
속성과 인덱스 시그니처를 혼합해서 사용하는 일반적인 타입 시스템 기법 중 하나는 인덱스 시그니처의 원시 속성보다 명명된 속성에 대해 더 구체적인 속성 타입 리터럴을 사용하는 것입니다.
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 인덱스 시그니처의 타입으로 할당할 수 있어야 합니다.
// OKinterface MoreNarrowNumbers { [i: number]: string [i: string]: string | undefined}
// OKconst mixesNumbersAndStrings: 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}인터페이스 확장
때로는 서로 형태가 비슷한 여러 개의 인터페이스를 갖게 됩니다.
타입스크립트는 인터페이스가 다른 인터페이스의 모든 멤버를 복사해서 선언할 수 있는 확장된 인터페이스를 허용합니다.
확장할 인터페이스의 이름 뒤에 extends 키워드를 추가해서 다른 인터페이스를 확장한 인터페이스라는 걸 표시합니다.
이렇게 하면 파생 인터페이스를 준수하는 모든 객체가 기본 인터페이스의 모든 멤버도 가져야 한다는 것을 타입스크립트에 알려줍니다.
interface Writing { title: string}
interface Novella extends Writing { pages: number}
// OKlet myNovella: Novella = { pages: 195, title: 'Ethan Frome',}인터페이스 확장은 프로젝트의 한 엔티티 타입이 다른 엔티티의 모든 멤버를 포함하는 상위 집합을 나타내는 실용적인 방법입니다. 인터페이스 확장으로 여러 인터페이스에 관계를 나타내기 위해 동일한 코드를 사용하는 것을 피할 수 있습니다.
재정의된 속성
파생 인터페이스는 다른 타입으로 속성을 다시 선언해 기본 인터페이스의 속성을 재정의하거나 대체할 수 있습니다. 타입스크립트의 타입 검사기는 재정의된 속성이 기본 속성에 할당되어 있도록 강요합니다. 이렇게 하면 파생 인터페이스 타입의 인스턴스를 기본 인터페이스 타입에 할당할 수 있습니다.
속성을 재선언하는 대부분의 파생 인터페이스는 해당 속성을 유니언 타입의 더 구체적인 하위 집합으로 만들거나 속성을 기본 인터페이스의 타입에서 확장된 타입으로 만들기 위해 사용합니다.
interface WithNullableName { name: string | null}
interface WithNonNullableName extends WithNullableName { name: string}
interface WithNumericName extends WithNullableName { // Error: Interface 'WithNumericName' incorrectly extends interface 'WithNullableName'. // Types 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}다중 인터페이스 확장
인터페이스는 여러 개의 다른 인터페이스를 확장해서 선언할 수 있습니다. 파생 인터페이스는 모든 기본 인터페이스의 모든 멤버를 받습니다.
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}인터페이스 병합
인터페이스의 중요한 특징 중 하나는 서로 병합하는 능력입니다. 두 개의 인터페이스가 동일한 이름으로 동일한 스코프에 선언된 경우, 선언된 모든 필드를 포함하는 더 큰 인터페이스가 코드에 추가됩니다.
interface Merged { fromFirst: string}
interface Merged { fromSecond: number}
// interface Merged {// fromFirst: string// fromSecond: number// }일반적으로 인터페이스가 여러 곳에 선언되면 코드를 이해하기 어려워 가능하면 인터페이스 병합을 사용하지 않는 것이 좋습니다.
그러나 인터페이스 병합은 외부 패키지 또는 Window 같은 내장된 전역 인터페이스를 보강하는 데 유용합니다.
interface Window { myEnvironmentVariable: string}
window.myEnvironmentVariable // 타입: string이름이 충돌되는 멤버
병합된 인터페이스는 타입이 다른 동일한 이름의 속성을 여러 번 선언할 수 없습니다. 속성이 이미 인터페이스에 선언되어 있다면 나중에 병합된 인터페이스에서도 동일한 타입을 사용해야 합니다.
interface MergedProperties { same: (input: boolean) => string different: (input: string) => string}
interface MergedProperties { same: (input: boolean) => string // ok different: (input: number) => string // Error: Subsequent property declarations must have the same type. // Property 'different' must be of type '(input: string) => string', // but here has type '(input: number) => string'.}그러나 병합된 인터페이스는 동일한 이름과 다른 시그니처를 가진 메서드는 정의할 수 있습니다. 이렇게 하면 메서드에 대한 함수 오버로드가 발생합니다.
interface MergedMethods { different(input: string): string}
interface MergedMethods { different(input: number): string // OK}마치며
- 타입 별칭 대신 인터페이스를 사용한 객체 타입 선언하기
- 선택적 속성, 읽기 전용 속성, 함수, 메소드 등 다양한 인터페이스 속성 타입 소개
- 객체의 포괄적인 속성을 담기 위한 인덱스 시그니처 사용하기
- 중첩된 인터페이스와
extends상속 확장으로 인터페이스 재사용하기 - 동일한 이름의 인터페이스 병합하기