우리가 직접 만들 수 있는데 왜? 지루한 내장타입 형태만 사용??
타입별칭 vs 인터페이스
* born: number 와 name: string 을 가진 객체를 타입 별칭으로 구현하는 구문
type Poet = {
born: number;
name: string;
};
* 인터페이스로 구현한 동일한 구문
interface Poet {
born: number;
name: string;
};
- 세미콜론(;)을 선호한다면, 대부분 인터페이스 뒤가 아닌 타입별칭 뒤에 세미콜론을 넣는다.
- 이 기본설정은 세미콜론을 사용해 변수를 선언하는 것과 세미콜론 없이 클래스 또는 함수를 선언하는 것의 차이를 반영한다.
//2.
const valueLater: Poet;
valueLater = {
born: 1935,
naem: 'Sara Teasdale',
};
valueLater = 'Emily Dickinson';
//Error: Type 'string' is not assignable to 'Poet'.
valueLater = {
born: true,
//Error: Type 'boolean' is not assignable to type 'number'.
name: 'Sappho'
};
- 인터페이스에 대한 타입스크립트의 할당가능성 검사와 오류메시지는 객체타입에서 실행되는 것과 동일하다.
- Poet이 인터페이스 또는 타입별칭인 경우 valueLater 변수에 할당하는 것에 대한 할당 가능성 오류는 거의 동일하다.
* 인터페이스와 타입별칭의 주요 차이점
- 인터페이스는 속성증가를 위해 병합merge할 수 있다, 이 기능은 전역 인터페이스 또는 npm패키지와 같은 외부 코드를 사용할 때 유용하다.
- 인터페이스는 클래스가 선언된 구조의 타입을 확인하는데 사용할 수 있지만 타입별칭은 사용할 수 없다.
- 일반적으로 인터페이스에서 타입스크립트 타입 검사기가 더 빨리 작동함.
- 인터페이스는 타입 별칭이 하는 것처럼 새로운 객체 리터럴의 동적인 복사 붙여놓기보다 내부적으로 더 쉽게 캐시할 수 있는 명령 된 타입을 선언한다.
- 인터페이스는 이름 없는 객체 리터럴의 별칭이 아닌 이름 있는(명명된) 객체로 간주되므로 어려운 특이케이스에서 나타나는 오류메시지를 좀 더 쉽게 읽을 수 있다.
- 타입별칭의 유니언타입과 같은 기능이 필요할 때까지는 인터페이스를 사용하는것이 좋다.
속성타입
선택적 속성
//1.
interface Book {
author?: string;
pages: number;
};
const ok: Book = {
author: 'Rita Dove',
pages: 80
};
const missing: Book = {
pages: 80
}
- Book 인터페이스는 필수 속성 pages와 선택적속성 author를 가진다.
- Book 인터페이스를 사용하는 객체에 필수 속성만 제공된다면 선택적 속성은 제공되거나 생략가능.
- undefined 를 포함한 유니언 타입의 선택적 속성과 일반 속성 사이의 차이점과 관련된 주의사항은 객체타입뿐만 아니라 인터페이스에도 적용됨.
읽기전용속성
//1.
interface Page {
readonly text: string;
}
const read = (page: Page) => {
//text 속성을 수정하지 않고 읽는 것
console.log(page.text);
page.text += '!';
//Error: Cannot assign to 'text' because it is read-only property
}
- Page 인터페이스의 text 속성에 접근하면 string을 반환하지만, text에 새로운 값을 할당하면 타입오류 발생한다.
- readonly 제한자는 타입시스템에만 존재하며 인터페이스에서만 사용가능
- readonly 제한자는 객체의 인터페이스를 선언하는 위치에서만 사용되고 실제 객체에는 적용되지 않는다.
//2.
const pageIsh = {
text: 'Hello, world'
}
// pageIsh는 Page객체가 아니라 text가 있는, 유추된 객체타입이다.
pageIsh.text += '!';
//pageIsh의 더 구체적인 버전인 Page를 읽는다.
read(pageIsh);
- Pate예제에서 text속성의 부모객체는 함수 내부에서 text로 명시적으로 사용되지 않았기 때문에 함수 밖에서 속성을 수정할 수 있다.
- 쓰기가능한 속성을 readonly속성에 할당할 수 있으므로 pageIsh는 Page로 사용할 수 있다.
- 가변속성은 readonly 속성이 필요한 모든 위치에서 읽을 수 있다.
- 명시적타입 애너테이션인 : Page로 변수 pageIsh를 선언하면 타입스크립트에 text 속성이 readonly라고 가리키게 된다.
- 하지만 유추된 타입은 readonly가 아님
- readonly 인터페이스 멤버는 코드영역에서 객체를 의도치않게 수정하는 것을 막는 편리한 방법이다.
- 그러나 readonly는 타입시스템 구성요소일 뿐 컴파일 된 자바스크립트 출력 코드에는 존재하지 않는다.
- readonly는 단지 타입스크립트 타입검사기를 사용해 개발 중에 그 속성이 수정되지 못하도록 보호하는 역할을 한다.
함수와 메서드
//1.
interface HasBothFunctionTypes {
property: () => string;
method(): string;
}
const hasBoth: HasBothFunctionTypes = {
property: () => '',
method() {
return '';
}
};
hasBoth.property();
hasBoth.method();
- method 와 property 멤버는 둘다 매개변수 없이 호출되어 string을 반환한다.
//2.
interface OptionalReadonlyFunctions {
optionalProperty?: () => string;
optionalMethod?(): string;
}
- 선택적 속성 키워드인 ?를 사용해 필수로 제공하지 않아도 되는 멤버로 나타낼 수 있다.
* 메서드와 속성선언은 대부분 서로 교환하여 사용할 수 있다. 메서드와 속성의 주요 차이점은?????
(1) 메서드는 readonly로 선언할 수 없지만, 속성은 가능
(2) 인터페이스 병합은 메서드와 속성을 다르게 처리한다.
(3) 타입에서 수행되는 일부 작업은 메서드와 속성을 다르게 처리한다.
* 기본함수가 this 를 참조할 수 있다는 것을 알고 있다면 메서드 함수를 사용. 가장 일반적으로 클래스의 인스턴스에서 사용된다.
* 반대의 경우는 속성함수를 사용
호출 시그니처
//1.
type FunctionAlias = (input: string) => number;
interface CallSignature {
(input: string): number;
}
//타입: (input: string) => number
const typedFunctionAlias: FumctionAlias = (input) => input.length;
//타입: (input: string) => number
const typedCallSignature: CallSignature = (input) => input.length;
- FunctionAlias 와 CallSignature 타입은 모두 동일한 함수 매개변수와 반환타입을 설명한다.
- 호출시그니처는 사용자 정의 속성을 추가로 갖는 함수를 설명하는데 사용할 수 있다.
- 타입스크립트는 함수 선언에 추가된 속성을 해당 함수 선언의 타입에 추가하는 것으로 인식한다.
//2.
interface FunctionWithCount {
count: number;
(): void;
}
const hasCallCount: FunctionWithCount;
const keepsTrackOfCalls = () => {
keepsTrackOfCalls.count += 1;
console.log('I've been called ${keepsTrackOfCalls.count} times!');
}
keepsTrackOfCalls.count = 0;
hasCallCount = keepsTrackOfCalls;
const doesNotHaveCount = () => {
console.log('No idea!');
}
hasCallCount = doesNotHaveCount;
//Error: Property 'count' is missing in type '() => void' but required in type 'FunctionWithCall'
- keepsTrackOfCalls 함수 선언에는 number 타입인 counter 속성이 주어져 FunctionWithCount 인터페이스에 할당할 수 있다.
- 따라서 FunctionWithCount 타입의 hasCallCount 인수에 할당할 수 있다.
- 마지막 함수에는 count 속성이 주어지지 않았음.
인덱스시그니처
//1.
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 'number'.
- WordCounts 인터페이스는 number 값을 갖는 모든 string 키를 허용하는 것으로 선언되어 있다.
- 이런 타입의 객체는 값이 number 이면 string 키가 아닌 그 어떤 키도 바인딩할 수 없다.
- 인덱스시그니처는 객체에 값을 할당할 때 편리하지만 타입 안정성을 완벽하게 보장하지는 않는다.
- 인덱스시그니처는 객체가 어떤 속성에 접근하든 간에 값을 반환해야함을 나타낸다.
//2.
interface DatesByName {
[i: string]: Date;
}
const publishDates: DateByName = {
Frankenstein: new Date('1 January 1818')
}
publishDates.Frankenstein;
console.log(publishDates.Frankenstein.toString())
publishDates.Beloved;
console.log(publishDates.Beloved.toString())
// 타입시스템에서는 오류가 나지 않지만 실제 런타임에서는 오류발생함.
// Runtime Error: Cannot read property 'toString' of undefined (reading publishDates.Beloved)
- publishDates 값은 Date 타입으로 Frankenstein 을 안전하게 반환하지만 타입스크립트는 Beloved가 정의되지 않았음에도 불구하고 정의되었다고 생각하도록 속인다.
- 키/값 쌍을 저장하려고 하는데 키를 미리 알 수 없다면 Map을 사용하는게 더 안전함.
- .get메서드는 항상 키가 존재하지 않음을 나타내기 위해서 |undefined 타입을 반환한다.
속성과 인덱스 시그니처 혼합
//1.
interface HistoricalNovels {
Oroonoko: number;
[i: string]: number;
}
const novels: HistoricalNovels = {
Outlander: 1991,
Oroonoko: 1688
}
const missingOroonoko: HistoricalNovels = {
//Error: Property 'Oroonoko' is missing in type
// '{Outlander: number}' but required in type 'HistoricalNovels'.
Outlander: 1991
}
- HistoricalNovels 는 모든 속성을 number 타입으로 선언했고 추가적으로 Oroonoko 속성이 존재해야한다.
- 속성과 인덱스시그니처를 혼합해서 사용하는 일반적인 타입시스템기법 중 하나는 인텍스 시그니처의 원시속성보다 명명된 속성에 대해 더 구체적인 속성 타입 리터럴을 사용하는 것이다.
- 명명된 속성으 ㅣ타입이 인덱스 시그니처에 할당될 수 있는 경우(각각의 리터렁 및 원시 속성에 해당) 타입스크립트는 더 구체적인 속성 탕비 리터럴을 사용하는 것을 허용한다.
//2.
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 assignavle to type '0'.
}
- ChapterStarts는 preface 속성은 0으로, 다른 모든 속성은 더 일반적인 number를 갖도록 선언한다.
- 즉, ChapterStarts를 사용하는 모든 객체의 preface 속성은 반드시 0 이어야 함.
숫자 인덱스 시그니처
//1.
interface MoreNarrowNumbers {
[i: number]: string;
[i: string]: string | undefined;
}
const mixesNumbersAndStrings: MoreNarrowNumbers = {
0: '',
key1: '',
key2: undefined
}
interface MoreNarrowStrings {
[i: number]: string | undefined;
// Error: 'number' index type 'string | undefined' is not assignabl to 'string' index type 'string'.
[i: string]: string;
}
- MoreNarrowNumbers 인터페이스는 string을 string | undefined 에 할당할 수 있지만,
- MoreNarrowStrings 인터페이스는 string | undefined 를 string에 할당할 수 없다.
중첩 인터페이스
interface Novel {
author: {
name: string;
};
setting: Setting;
}
interface Setting {
place: string;
year: number;
}
const myNovel: Novel;
myNovel = {
author: {
name: 'Jane'
},
setting: {
place: 'England',
year: 1812
}
}
myNovel = {
author: {
name: 'Emily Bronte'
},
setting: {
//Error: Property 'year' is missing in type '{place: string;}' but required in type 'Setting'.
place: 'West Yorkshire'
}
}
- Novel 인터페이스는 인라인 객체 타입인 author 속성과 Setting 인터페이스인 setting 속성을 포함한다.
인터페이스 확장
extends
키워드를 추가해서 다른 인터페이스를 확장한 인터페이스라는걸 표시한다.//1.
interface Writing {
title: string;
}
interface Novella extends Writing {
pages: number;
}
const myNovella: Novella = {
pages: 195,
title: 'Ethan'
}
const missingPages: Novella = {
title: 'Awakening'
// Error: Property 'pages' is missing in type '{title: string}' but required in type 'Novella'
}
const 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: 'Natural'
}
- 인터페이스 확장은 프로젝트의 한 엔티티entity 타입이 다른 엔티티의 모든 멤버를 포함하는 상위 집합을 나타내는 실용적 방법/
재정의 된 속성
interface WithNullableName {
name: string | null;
}
interface WithNonNullableName extends WithNullableName {
name: string;
}
interface WithNumericName extends WithNullableName {
name: number | string
// 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'
}
- WithNullableName 타입은 WithNonNullableName 에서 null 을 허용하지 않도록 다시 설정되었다.
- 그러나, WithNumericName 의 name 에는 number | string 이 허용되지 않음.
- number | string 은, string | null 에 할당할 수 없기 때문.
다중인터페이스 확장
//1.
interface GivesNumber {
giveNumber(): number;
}
interface GivesString {
giveString(): string;
}
interface GivesBothAndEither extends GivesNumber, GivesString {
giveEither(): number | string;
}
const useGivesBoth = (instance: GivesBothAndEither) => {
instance.givesEither(); //타입: number | string
instance.giveNumber(); //타입: number
instance.giveString(); //타입: string
}
- GivesBothAndEither 는 세개의 메서드를 가진다.
- 1. 자체 메서드 2. GivesNumber 에서, 3. GivesString 에서 왔다.
- 여러 인터페이스를 확장하는 방식으로 인터페이스를 사용하면 코드 중복을 줄이고 다른 코드영역에서 객체의 형태를 더 쉽게 재사용 가능.
인터페이스 병합
//1.
interface Merged {
fromFirst: string;
}
interface Merged {
fromSecond: number;
}
- fromFirst 와 fromSecond 라는 두개의 속성을 갖는 Merged 인터페이스를 선언한다.
- 다음과 같다.
interface Merged {
fromFirst: string;
fromSecond: number;
}
- 일반적으로 타입스크립트 개발에서는 인터페이스 병합을 자주 사용하지는 않는다.
- 인터페이스가 여러곳에 선언되면 코드를 이해하기 어려워지므로 가능하면 병합 사용하지 않는 것이 좋다.
//2.
interface Window {
myEnviromentVariable: string;
}
window.myEnviromentVariable; //타입: string
- 인터페이스 병합은 외부 패키지 또는 Window 같은 내장된 전역 인터페이스를 보강하는데 특히 유용하다.
- 기본 타입스크립트 컴파일러 옵션을 사용할 때, 파일에서 myEnviromentVariable 속성이 있는 Window 인터페이스를 선언하면 window.myEnviromentVariable 을 사용할 수 있다.
이름이 충돌되는 멤버
//1.
interface MergeProperties {
same: (input: boolean) => string;
different: (input: string) => string;
}
interface MergedProperties {
same: (input: boolean) => string;
different: (input: number) => string;
// Error: Subsequent property declaration must have the same type.
// Property 'different' must be of type '(input: string) => string'
// but here has type '(input: number) => string'
}
//3.
interface MergedMethods {
different(input: string): string;
}
interface MergedMethods {
different(input: number): string
}
- 병합된 인터페이스는 동일한 이름고 ㅏ다른 시그니처를 가진 메서드를 정의할 수 있고, 이렇게 하면 메서드에 대한 `함수오버로드`가 발생
- 위의 MergedMethods 인터페이스는 두가지 오버로드가 있는 different 메서드를 생성한다.