Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions chapter2/perfume/item12-14.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
## 💡 함수 표현식에 타입 적용하기

자바스크립트와 타입스크립트에서는 함수 '문장(statement)'과 함수 '표현식(expression)'을 다르게 인식합니다.

```
function rollDice1(sides:number): nimber {}
```

위 코드는 문장(statement)입니다. 반면 아래 코드들은 함수 표현식입니다.

```
const rollDice2 = function(sides: number): number {};
const rollDice3 = (sides: number) : number => {};
```

타입스크립트에서는 함수 표현식을 사용하는 것이 좋습니다. 함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있기 때문입니다.

type DiceRollFn = (sides: number) => number;
const rollDice: DiceRollFn = sides => {};

sides에 마우스를 올려 보면 이미 타입스크립트가 sides의 타입을 number로 인식하고 있다는 걸 알 수 있습니다.

함수 타입의 선언은 불필요한 코드의 반복을 줄입니다.

```
function add(a:number, b: number) {return a + b};
function sub(a:number, b: number) {return a - b};
function mul(a:number, b: number) {return a * b};
function div(a:number, b: number) {return a / b};

```

위의 코드처럼 반복되는 함수 시그니처를 일일이 적어줄 필요 없이 하나의 함수 타입으로 통합할 수도 있습니다.

```
type BinaryFn = (a:number, b: number) => number;
const add: BinaryFn = (a,b) => a+b;
const sub: BinaryFn = (a,b) => a-b;
const mul: BinaryFn = (a,b) => a*b;
const div: BinaryFn = (a,b) => a/b;
```

이처럼 함수의 매개변수에 타입 선언을 하는 것보다 함수 표현식 전체 타입을 정의하는 것이 코드도 간결하고 안전합니다. 다른 함수의 시그니처와 동일한 타입을 가지는 새 함수를 작성하거나, 동일한 타입 시그니처를 가지는 여러 개의 함수를 작성할 때는 매개변수의 타입과 반환 타입을 반복해서 작성하지 말고 함수 전체의 타입 선언을 적용해야 합니다.

## 💡타입과 인터페이스의 차이점 알기

타입스크립트에서 명명된 타입(named type)을 정의하는 두 가지 방법이 있습니다.

1. type

```
type TState = {
name: string;
capital: string;
}
```

2. interface

```
interface Istate {
name: string;
capital: string;
}
```

대부분의 경우 두 가지 중 어느 것을 사용해도 상관 없습니다. 하지만 둘의 차이를 분명하게 알고, 같은 상황에서는 동일한 방법으로 명명된 타입을 정의해 일관성을 유지해야 합니다.

그럼 둘의 차이가 뭘까요?

인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지는 못한다는 것입니다. 복잡한 타입을 확장하고 싶다면 타입과 &을 사용해야 합니다. 유니온 타입은 있지만, 유니온 인터페이스라는 개념은 존재하지 않죠. 그래서 type 키워드는 일반적으로 interface보다 쓰임새가 많습니다. type 키워드는 유니온이 될 수도 있고, 매핑된 타입 또는 조건부 타입 같은 고급 기능에 활용되기도 합니다.

대신 인터페이스에는 타입에 없는 몇 가지 기능이 있습니다. 그중 하나가 바로 보강(augment)이 가능하다는 것입니다. 아까 interface를 설명할 때 등장했던 예제에 population 필드를 추가할 때 보강 기법을 사용할 수 있습니다.

```
interface Istate {
name: string;
capital: string;
}

interface Istate {
population: number;
}

const wyoming: IState = {
name: 'Wyoming',
capital: 'Cheyenne',
population: 500_000
};

```

이 예제처럼 속성을 확장하는 것을 '선언 병합(declaration merging)'이라고 합니다. 선언 병합을 지원하기 위해서는 반드시 인터페이스를 사용해야 합니다.

자, 그럼 타입과 인터페이스 중 어느 것을 사용하는 게 좋을까요?

복잡한 타입이라면 고민할 것도 없이 타입 키워드를 사용하면 됩니다. 그러나 두 가지 방법으로 모두 표현할 수 있는 간단한 객체 타입이라면 일관성과 보강의 관점에서 고려해 봐야 합니다. 기존의 코드베이스에서 사용하는 키워드를 쓰는 것이 좋습니다.

## 💡 타입 연산과 제너릭 사용으로 반복 줄이기

같은 코드를 반복하지 말라는 DRY(don't repeat yourself) 원칙이 있습니다. 이 원칙은 타입에도 적용됩니다. 같은 타입을 반복해서 적어줄 필요는 없겠죠! 반복을 줄이는 가장 간단한 방법은 타입에 이름을 붙이는 것입니다. 예를 들어, 아래 코드처럼 몇 개의 함수가 같은 타입 시그니처를 공유하고 있다고 해 보겠습니다.

```
function get(url:string, opts: Options) : Promise<Response> {}
function post(url:string, opts: Options) : Promise<Response> {}
```

위와 같은 경우 해당 시그니처를 명명된 타입으로 분리할 수 있습니다.

```
type HTTPFunction = (url:string, opts: Options) => Promise<Response>;
const get: HTTPFunction = (url, opts) => {};
const post: HTTPFunction = (url, opts) => {};

```

한 인터페이스가 다른 인터페이스를 확장하게 해서 반복을 제거할 수도 있습니다.

```
interface Person {
firstName: string;
lastName: string;
}

interface PersonWithBirthDate extends Person {
birth: Date;
}
```

이처럼 extends를 사용하면 인터페이스 필드의 반복을 피할 수 있습니다.

뿐만 아니라 우리에게는 제너릭이 있습니다. 제너릭 타입은 타입을 위한 함수와 같습니다. 그리고 함수는 코드에 대한 DRY 원칙을 지킬 때 유용하게 사용됩니다. 따라서 타입의 반복을 줄이는 핵심에 제너릭이 있습니다. 타입을 반복하는 대신 제너릭 타입을 사용하여 타입들 간에 매핑을 하는 것이 좋습니다. 그런데 함수에서 매개변수로 매핑할 수 있는 값을 제한하기 위해 타입 시스템을 사용하는 것처럼, 제너릭 타입에서 매개변수를 제한할 수 있는 방법이 필요합니다. 그 방법은 바로 extends입니다. extends를 이용하면 제너릭 매개변수가 특정 타입을 확장한다고 선언할 수 있습니다.
25 changes: 25 additions & 0 deletions chapter2/perfume/item15-18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## 💡 동적 데이터에 인덱스 시그니처 사용하기

런타임 때까지 객체의 속성을 알 수 없을 경우에만 인덱스 시그니처를 사용하는 것이 좋습니다.

#### 🤔 인덱스 시그니처란?

인덱스 시그니처는 {[키의 이름: 키의 타입]: 값의 타입} 과 같은 형태를 가진 타입 문법을 말합니다. 유연한 매핑을 표현할 수 있다는 장점을 가지고 있죠. 하지만 유연한만큼 잘못된 키를 포함한 모든 키를 허용한다는 단점이 있습니다.

그래서 인덱스 시그니처의 값 타입에 undefined를 추가해서 안정성을 높이는 방법을 권장합니다. 하지만 더욱 추천하는 것은, 가능하다면 인터페이스나 Record, 매핑딘 타입 같은 인덱스 시그니처말고 정확한 타입을 사용하는 것입니다.

## 💡 number 인덱스 시그니처보다는 Array, 튜플, ArrayLike 사용하기

배열은 객체이므로 키는 숫자가 아니라 문자열입니다. 자바스크립트는 숫자를 키로 사용하는 것을 허용하지 않습니다. 숫자 인덱스를 사용해도 인덱스들이 문자열로 자동으로 변환되어 사용됩니다. 이런 혼란을 바로잡기 위해 타입스크립트는 숫자 키를 허용합니다. 하지만 인덱스 시그니처로 사용된 number 타입은 버그를 잡기 위한 순수 타입스크립트 코드입니다. 그러니 인덱스 시그니처에 number를 사용하기보다 Array, 튜플, ArrayLike를 사용하기를 권장합니다.

## 💡 변경 관련된 오류 방지를 위해 readonly 사용하기

만약 함수가 매개변수를 수정하지 않는다면 readonly로 선언하는 것이 좋습니다. readonly 매개변수는 인터페이스를 명확하게 하며, 매개변수가 변경되는 것을 방지합니다. 불변을 사랑하는 함수형 개발자들이 readonly를 사랑하는 것은 당연할 수 밖에 없습니다.

readonly를 사용하게 되면 변경하면서 발생하는 오류를 방지할 수 있고, 변경이 발생하는 코드도 쉽게 찾을 수 있습니다.

하지만 readonly는 얕게 동작한다는 것도 잊지 마세요!

## 💡 매핑된 타입을 사용하여 값을 동기화하기

매핑된 타입을 사용해 관련된 값과 타입을 동기화하도록 하세요. 새로운 속성이 추가될 때마다 값과 타입을 동기화시키면 타입 체커에게 보다 정확한 정보를 줄 수 있게 됩니다. 또한 매핑된 타입은 한 객체가 또 다른 객체와 정확히 같은 속성을 가지게 할 때 이상적입니다. 매핑된 타입을 이용하면 타입스크립트가 코드에 제약을 강제하도록 할 수 있습니다.
51 changes: 51 additions & 0 deletions chapter2/perfume/item9-11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
### 💡 타입 단언보다는 타입 선언을 사용하기

지난 글에서 타입 스크립트를 사용하는 이유는 타입 안정성을 높이기 위해서라고 말한 적이 있습니다. 보다 높은 타입 안정성을 위해, 타입 단언보다는 타입 선언을 사용하는 것이 좋습니다. 왜 그런지 아래 코드를 함께 살펴보며 확인해봅시다.

```
interface Cat {name: string};

const cherie: Cat = {name: 'Cherie'}; // 타입은 Cat
const homie = {name: 'Homie'} as Cat; // 타입은 Cat
```

첫 번째 cherie: Cat은 타입 선언입니다. 변수에 타입 선언을 붙여서 그 값이 선언된 타입임을 명시하는 방법이죠. 두 번째 as Cat은 타입 단언입니다. 말 그대로 타입을 '단언'했기 때문에 타입 스크립트가 추론한 타입이 있더라도 그걸 무시하고 Cat 타입으로 간주합니다. 그래서 아래와 같은 일이 일어납니다.

```
const cherie: Cat = {};
```

위 코드를 작성할 경우 'Cat' 유형에 필요한 'name' 속성이 없다는 에러 메시지가 뜹니다. 타입 스크립트가 할당되는 값이 해당 인터페이스를 만족하는지 검사했기 때문입니다. 반면 아래 코드는 에러가 발생하지 않습니다.

```
const homie = {} as Cat;
```

강제로 타입을 지정했기 때문에 타입 체커가 오류를 무시한 것입니다. 이러한 이유로 타입스크립트보다 타입 정보를 더 잘 알고 있는 상황에만 타입 단언문을 사용하는 것이 좋습니다.

#### ➕ 화살표 함수의 반환 타입 선언

화살표 함수의 타입 선언이 다소 까다롭기 때문에 따로 다뤄보겠습니다. 화살표 함수 안에서 타입과 함께 변수를 선언하는 것이 가장 직관적입니다.

```
const cats = ['cherie', 'homie', 'honey'].map(name=> {
const cat:Cat = {name};
return cat
});
```

그러나 이 방식은 조금 번잡해보인다는 단점이 있습니다. 코드를 좀 더 간결하게 만들어보겠습니다.

```
const cats:Cat[] = ['cherie', 'homie', 'honey'].map(
(name):Cat -> ({name})
);
```

💥 **Boom!** 우리가 원하는 타입을 직접 명시하고, 타입스크립트가 할당문의 유효성을 검사하게 만들었습니다.

### 💡 잉여 속성 체크의 한계 인지하기

타입이 명시된 변수에 객체 리터럴을 할당할 때 타입스크립트는 해당 타입의 속성이 있는지, 그리고 '그 외의 속성은 없는지' 확인합니다. 이를 잉여 속성 체크라고 부르는데요, 잉여 속성 체크 역시 조건에 따라 동작하지 않는다는 한계가 있고, 일반적인 할당 가능 검사와 함께 쓰이면 구조적 타이핑이 무엇인지 혼란스러워질 수 있습니다. **잉여 속성 체크는 할당 가능 검사와는 별도의 과정이라는 것**을 기억하세요.

잉여 속성 체크는 구조적 타이핑 시스템에서 허용되는 속성 이름의 오타 같은 실수를 잡는 데 효과적인 방법입니다. 하지만 적용 범위가 매우 제한적이며 오직 객체 리터럴에만 적용됩니다. 임시 변수를 도입하면 잉여 속성 체크를 건너뛸 수 있다는 점을 기억해야 합니다.