해당 포스트는
러닝 타입스크립트7장을 읽고 정리한 포스트입니다.
책의 모든 내용을 작성하는 것이 아닌 주관적인 기준에 따라 필요한 정보만 정리했습니다.
🍻 타입 별칭 vs 인터페이스
인터페이스와 타입 별칭은 거의 동일하게 동작합니다.
 하지만 미묘한 차이가 있고 그 차이를 이해하고 상황에 맞게 사용하는 것이 중요합니다.
- 인터페이스의 특징- 인터페이스에서 타입 검사기가 더 빨리 동작함
- 인터페이스는 오류 메시지를 더 쉽게 읽을 수 있음
- 인터페이스는 속성 증가를 위해 병합할 수 있음
- 인터페이스는 클래스의 타입 구조를 확인하는데 사용 가능 ( 8장 )
- readonly사용 가능
 
- 인터페이스에서 타입 검사기가 더 빨리 동작함
1과 2만 봐도 타입 별칭보다는 인터페이스를 사용하는 것이 더 좋은 것 같습니다.
 ( 특별한 이유가 있지 않다면 인터페이스를 사용하는 것이 성능에서도 좋고 의미적으로도 좋은 것 같습니다. )
0️⃣ 타입 별칭에서만 할 수 있는 것
그렇다면 인터페이스는 할 수 없고, 타입 별칭에서만 할 수 있는 것은 무엇이 있을까요?
(1)처럼 타입의 유니언을 사용하는 경우에 인터페이스는 하나의 깊이를 더 들어가야만 구현할 수 있는 차이가 있습니다.
 하지만 타입 별칭을 사용하면 깊이 없이 바로 타입의 유니언을 사용할 수 있습니다.
이런 특별한 경우를 제외한다면 인터페이스를 사용하는 것이 좋은 것 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
interface XPos {
  x: number;
}
interface YPos {
  y: number;
}
interface IPos1 extends XPos, YPos {
  z: number;
}
interface IPos2 {
  // (1)
  pos: XPos | YPos;
  z: number;
}
// (1)
type TPos = { z: number } & (XPos | YPos);
const iPos1: IPos1 = {
  x: 1,
  y: 1,
  z: 1,
};
const iPos2: IPos2 = {
  pos: {
    x: 1,
    // y: 1,
  },
  z: 1,
};
const tPos: TPos = {
  x: 1,
  // y: 1,
  z: 1,
};
🥥 속성 타입
0️⃣ 선택적 속성
선택적 속성은 생략될 수 있습니다.
1
2
3
4
5
6
7
8
9
interface Pos {
  x: number;
  y?: number;
}
const pos: Pos = {
  x: 1,
  // y: 1,
};
1️⃣ 읽기 전용 속성
속성 앞에 readonly를 사용하면 다른 값으로 설정될 수 없음((1))을 설정할 수 있습니다.
 TypeScript에서만 존재하고 인터페이스에서만 사용할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
interface Pos {
  readonly x: number;
}
const pos: Pos = {
  x: 1,
};
// Cannot assign to 'x' because it is a read-only property
pos.x = 10;
읽기 전용 속성과 읽기 전용이 아닌 속성을 서로 할당할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface ReadOnlyPos {
  readonly x: number;
}
interface Pos {
  x: number;
}
let readonlyPos: ReadOnlyPos = {
  x: 1
};
let pos: Pos = {
  x: 1
}
readonlyPos = pos;
// readonlyPos.x = 10;
pos = readonlyPos;
pos.x = 10;
아래 예시는 함수의 매개변수로 받는 객체에 readonly를 사용해서 객체를 안전하게 보호하는 예시입니다.
(2)에서 선언한 변수는 readonly가 없는 객체 타입이고 (3)의 read()는 readonly인 프로퍼티를 갖는 객체를 매개변수로 받는 함수입니다.
 이 함수는 함수의 선언만 보고 매개변수가 변하지 않음을 보증할 수 있습니다.
 ( 넣어주는 인자(2)가 readonly가 아니지만 보증할 수 있습니다. )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Page {
  readonly text: string;
}
interface ReadHanlder {
  (page: Page): void;
}
// (3)
const read: ReadHanlder = (page) => {
  // (1) Error: Cannot assign to 'text' because it is a read-only property.
  page.text = "쿠키... 헤더에 담았지...";
};
// (2)
const pageIsh = {
  text: "첫 번째 페이지",
};
pageIsh.text += " + 수정";
read(pageIsh);
2️⃣ 함수와 메서드
TypeScript의 인터페이스에서는 함수 선언의 두 가지 방법을 제공합니다.
- 메서드 구문: func(): void형태로 사용
- 속성 구문: func: () => void형태로 사용
1
2
3
4
5
6
7
interface Func {
  // 메서드 구문
  mFunc(): void;
  // 속성 구문
  aFunc: () => void;
}
- 차이점- 속성만 readonly사용 가능
- 인터페이스 병합에서 다르게 처리
- 일부 작업에서 다르게 처리 ( 15장 )
 
- 속성만 
this를 참조할 수 있다는 것을 알고 있다면 메서드 함수 사용하고, 그 외의 경우에는 속성 함수 사용하는 것이 좋다고 합니다.
 대부분 this를 사용하지 않으니까 속성 함수를 사용하는 것이 좋은 것 같습니다.
3️⃣ 호출 시그니처
호출 시그니처는 값을 함수처럼 호출하는 방식에 대한 타입 시스템 설명입니다.
 즉, 인터페이스로 함수의 타입을 구현하는 것을 의미합니다.
메서드 구문에서 함수의 이름 즉, 프로퍼티의 이름만 제외하면 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 함수면서 "count"를 프로퍼티를 갖고 있다는 의미
 */
interface FunctionWithCount {
  // 메서드 구문 -> func(): void
  (): void;
  count: number;
}
const keepsTrackOfCalls = () => {
  keepsTrackOfCalls.count += 1;
  console.log("count >> ", keepsTrackOfCalls.count);
};
keepsTrackOfCalls.count = 0;
const hasCallCount1: FunctionWithCount = keepsTrackOfCalls;
// Error: Property 'count' is missing in type '() => void' but required in type 'FunctionWithCount'.
const hasCallCount2: FunctionWithCount = () => {
  keepsTrackOfCalls.count += 1;
  console.log("count >> ", keepsTrackOfCalls.count);
};
// hasCallCount2.count = 0;
4️⃣ 인덱스 시그니처
객체가 임의의 키를 받고 특정 타입을 반환할 수 있음을 타입으로 정의하는 방법입니다.
1
2
3
4
5
6
7
8
9
10
11
interface WordCounts {
  [key: string]: number;
}
const counts: WordCounts = {};
counts.apple = 0;
counts.blue = 1;
// Error: Type 'string' is not assignable to type 'number'.
counts.color = "red";
매우 유용해보이지만 인덱스 시그니처는 타입 안정성을 완벽하게 보장해주지 않습니다.
 ( + 자동 완성 지원 X )
 (1)의 경우 존재하지 않는 프로퍼티지만 Date 타입으로 인식합니다.
 string인 어떤 key도 받을 수 있기 때문에 string인 key로 접근하면 검사하지 않고 있다고 판단하고 실행하게 됩니다.
1
2
3
4
5
6
7
8
interface DatesByName {
  [key: string]: Date;
}
const pulishDates: DatesByName = {};
// (1) date: Date
const date = pulishDates.Beloved;
키/값을 쌍으로 저장하지만 어떤 키가 들어올지 예측할 수 없다면 Map을 사용하는 것이 더 안전합니다. ( 9장 )
1. 속성과 인덱스 시그니처 혼합
인덱스 시그니처를 사용하면서 특정 속성을 받고 싶은 경우 아래처럼 사용하면 됩니다.
 단, 인덱스 시그니처의 타입과 특정 속성의 타입이 일치해야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Person {
  name: string;
  country: "kr" | "us";
  [key: string]: string;
}
const person1: Person = {
  name: "",
  country: "kr",
};
// Error: Property 'name' is missing in type '{ country: "us"; }' but required in type 'Person'
const person2: Person = {
  country: "us",
};
2. 숫자 인덱스 시그니처
JavaScript에서는 number 타입의 key를 가질 수 있지만, 암묵적으로 객체 key를 string으로 변환합니다.
따라서 number 인덱스 시그니처는 string 인덱스 시그니처의 타입에 포함될 수 있어야 합니다.
 ( (1)은 string | null이 string에 포함될 수 없기 때문에 오류 발생 )
1
2
3
4
5
6
7
8
9
10
11
// (1) 'number' index type 'string | null' is not assignable to 'string' index type 'string'
interface MoreNarrowNumber {
  [key: number]: string | null;
  [key: string]: string;
}
// (2)
interface MoreNarrowString {
  [key: number]: string;
  [key: string]: string | null;
}
5️⃣ 중첩 인터페이스
인터페이스도 다른 인터페이스를 객체 타입의 속성으로 가질 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Author {
  name: string;
}
interface Nobel {
  author: Author;
  page: number;
}
const nobel: Nobel = {
  author: {
    name: ""
  },
  page: 0
}
🍛 인터페이스 확장
extends를 사용하며 인터페이스가 다른 인터페이스의 모든 멤버의 정의를 복사해서 선언하는 방법입니다.
인터페이스 확장은 엔티티 타입이 다른 엔티티 타입의 모든 멤버를 포함하는 상위 집합을 나타내는 실용적인 방법입니다.
1
2
3
4
5
6
7
8
9
10
11
12
interface Person {
  age: number;
}
interface Author extends Person {
  name: string;
}
const author: Author = {
  name: "",
  age: 0,
};
0️⃣ 재정의된 속성
인터페이스 확장을 이용해서 이미 존재하는 프로퍼티를 재정의할 수 있습니다.
 하지만 이미 존재하는 프로퍼티를 수정하는 경우에는 더 구체적으로 재정의는 가능하지만, 더 넓은 타입으로 확장하는 것은 불가능합니다.
(1)의 경우에는 number | undefined를 number로 재정의하기 때문에 더 구체적으로 수정하는 것이라 가능합니다.
 하지만 (2)의 경우에는 number를 number | undefined로 확장하기 때문에 불가능합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// (1)
interface Person1 {
  age: number | undefined;
}
interface Author1 extends Person1 {
  name: string;
  age: number;
}
// (2)
interface Person2 {
  age: number;
}
interface Author2 extends Person2 {
  name: string;
  age: number | undefined;
}
/**
 * Interface 'Author2' incorrectly extends interface 'Person2'.
 * Types of property 'age' are incompatible.
 *   Type 'number | undefined' is not assignable to type 'number'.
 *     Type 'undefined' is not assignable to type 'number
 */
1️⃣ 다중 인터페이스 확장
,를 기준으로 여러 인터페이스를 작성하면 다중 인터페이스로 확장도 가능합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface MyString {
  s: string;
}
interface MyNumber {
  n: number;
}
interface MyValue extends MyString, MyNumber  {
  b: boolean;
}
const func = (v: MyValue) => {
  v.s;
  v.n;
  v.b;
}
🥗 인터페이스 병합
두 개의 인터페이스가 동일한 이름으로 동일한 스코프에 선언되는 경우 선언된 모든 필드를 포함하는 더 큰 인터페이스로 병합됩니다.
1
2
3
4
5
6
7
8
9
10
11
interface Merged {
  s: string;
}
interface Merged {
  n: number;
}
const merged: Merged = {
  s: "",
  n: 0,
};
일반적으로 위와 같이 사용하기 보다는 외부에서 받은 패키지에 뭔가를 덧붙이는 경우 사용합니다.
1
2
3
4
5
interface Window {
  myValue: string;
}
window.myValue;
0️⃣ 이름이 충돌되는 멤버
병합된 인터페이스는 같은 이름의 멤버를 여러 번 선언할 수 없습니다.
 만약에 사용한다면 나중에 병합된 인터페이스에서도 동일한 타입으로 선언해야 합니다.
하지만 메서드 방식을 사용하는 경우 함수 오버로드로 인지하고 정삭적으로 동작합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 속성 방식
interface Merged1 {
  func: (v: number) => string;
}
// Error: Subsequent property declarations must have the same type.
// Property 'func' must be of type '(v: number) => string',
// but here has type '(v: string) => string'.
interface Merged1 {
  func: (v: string) => string;
}
// 메서드 방식 ( 함수 오버로드 발생 )
interface Merged2 {
  func(v: number): string;
}
interface Merged2 {
  func(v: string): string;
}
const merged2: Merged2 = {
  func: (v: string | number) => "",
}
📮 레퍼런스
- « 러닝 타입스크립트 7장 » ( 조시 골드버그 지음, 고승원 옮김, 한빛미디어, 2023 ) 
