Skip to content

Commit 10eb35b

Browse files
committed
docs: Add docs about item12-14
1 parent 54880bf commit 10eb35b

1 file changed

Lines changed: 384 additions & 0 deletions

File tree

chapter2/lado/item12-14.md

Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
# 이펙티브 타입스크립트 아이템 12~14
2+
3+
## 아이템 12. 함수 표현식에 타입 적용하기
4+
5+
- 타입스크립트에서는 함수 표현식을 사용하는 것이 좋다. 함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있다는 장점이 있기 때문이다.
6+
7+
```tsx
8+
function rollDice(sides: number): number {
9+
/* ... */
10+
} // 문장
11+
const rollDice2 = function (sides: number): numberg {
12+
/* ... */
13+
} // 표현식
14+
const rollDice3 = (sides: number): number => {
15+
/* ... */
16+
} // 표현식
17+
18+
type DiceRollFn = (sides: number) => number
19+
const rollDice: DiceRollFn = (sides) => {
20+
/* ... */
21+
}
22+
```
23+
24+
- 함수 타입 선언의 장점
25+
26+
- 불필요한 코드의 반복을 줄인다.
27+
- 함수 구현부가 분리되어 있어 로직이 보다 분명해진다.
28+
29+
```tsx
30+
function add(a: number, b: number) {
31+
return a + b
32+
}
33+
function sub(a: number, b: number) {
34+
return a - b
35+
}
36+
function mul(a: number, b: number) {
37+
return a * b
38+
}
39+
function div(a: number, b: number) {
40+
return a / b
41+
}
42+
43+
type BinaryFn = (a: number, b: number) => number
44+
const add: BinaryFn = (a, b) => a + b
45+
const sub: BinaryFn = (a, b) => a - b
46+
const mul: BinaryFn = (a, b) => a * b
47+
const div: BinaryFn = (a, b) => a / b
48+
```
49+
50+
- 시그니처가 일치하는 다른 함수가 있을 때도 함수 표현식에 타입을 적용하면 좋다.
51+
52+
```tsx
53+
const responseP = fetch('/quote?by=Mark+Twain') // 타입은 Promise<Response>
54+
55+
async function getQuote() {
56+
const response = await fetch('/quote?by=Mark+Than')
57+
const quote = await response.json()
58+
return quote
59+
}
60+
61+
// {
62+
// "quote": "If you tell the truth, you don't have to remember anything.",
63+
// "source": "notebook",
64+
// "date": "1984"
65+
// }
66+
67+
// lib.dom.d.ts에 있는 fetch의 타입 선언
68+
declare function fetch(input: RequestInfo, init?: RequestInit): Promise<Response>
69+
70+
async function checkedFetch1(input: RequestInfo, init?: RequestInit) {
71+
const response = await fetch(input, init)
72+
if (!response.ok) {
73+
// 비동기 함수 내에서 거절된 프로미스로 변환합니다.
74+
throw new Error('Request failed: ' + response.status)
75+
}
76+
return response
77+
}
78+
79+
// typeof fn을 사용해 fetch 함수의 시그니처 참조
80+
const checkedFetch2: typeof fetch = async (input, init) => {
81+
const response = await fetch(input, init)
82+
if (!response.ok) {
83+
throw new Error('Request failed: ' + response.status)
84+
}
85+
return response
86+
}
87+
88+
// throw를 return으로 잘못 기재했을 때 타입 오류를 잡아냄
89+
const checkedFetch3: typeof fetch = async (input, init) => {
90+
// Type '(input: RequestInfo, init: RequestInit | undefined) => Promise<Response | Error>' is not assignable to type '{ (input: RequestInfo, init?: RequestInit | undefined): Promise<Response>; (input: RequestInfo, init?: RequestInit | undefined): Promise<...>; }'.
91+
// Type 'Promise<Response | Error>' is not assignable to type 'Promise<Response>'.
92+
// Type 'Response | Error' is not assignable to type 'Response'.
93+
// Type 'Error' is missing the following properties from type 'Response': headers, ok, redirected, status, and 11 more.
94+
const response = await fetch(input, init)
95+
if (!response.ok) {
96+
return new Error('Request failed: ' + response.status)
97+
}
98+
return response
99+
}
100+
```
101+
102+
<br />
103+
104+
## 아이템 13. 타입과 인터페이스의 차이점 알기
105+
106+
- 타입스크립트에서 명명된 타입을 정의하는 방법은 두가지가 있다. 대부분의 경우에는 타입을 사용해도 되고 인터페이스를 사용해도 된다.
107+
108+
```tsx
109+
type TState = {
110+
name: string
111+
capital: string
112+
}
113+
114+
interface IState {
115+
name: string
116+
capital: string
117+
}
118+
```
119+
120+
- (cf) 인터페이스 접두사로 I를 붙이는 것은 C#에서 비롯된 관례로, 지양해야 할 스타일이다.
121+
- 인터페이스 선언과 타입 선언의 비슷한 점
122+
123+
- 추가 속성과 함께 할당한다면 동일한 오류가 발생한다.
124+
125+
```tsx
126+
const wyoming: TState = {
127+
name: 'Wyoming',
128+
capital: 'Cheyenne',
129+
population: 500_000,
130+
// Type '{ name: string; capital: string; population: number; }' is not assignable to type 'TState'.
131+
// Object literal may only specify known properties, and 'population' does not exist in type 'TState'.
132+
}
133+
```
134+
135+
- 인덱스 시그니처는 인터페이스와 타입에서 모두 사용할 수 있다.
136+
137+
```tsx
138+
type TDict = { [key: string]: string }
139+
interface IDict {
140+
[key: string]: string
141+
}
142+
```
143+
144+
- 함수 타입도 인터페이스나 타입으로 정의할 수 있다.
145+
146+
```tsx
147+
// 함수 타입에 추가적인 속성이 있을 때
148+
type TFnWithProperties = {
149+
(x: number): number
150+
prop: string
151+
}
152+
153+
interface IFnWithProperties {
154+
(x: number): number
155+
prop: string
156+
}
157+
```
158+
159+
- 제너릭이 가능하다.
160+
161+
```tsx
162+
type TPair<T> = {
163+
first: T;
164+
second: T;
165+
}
166+
167+
interface IPair<T> = {
168+
first: T;
169+
second: T;
170+
}
171+
```
172+
173+
- 클래스를 구현할 때, 타입과 인터페이스 둘 다 사용할 수 있다.
174+
175+
```tsx
176+
class StateT implements TState {
177+
name: string = ''
178+
capital: string = ''
179+
}
180+
181+
class StateI implements IState {
182+
name: string = ''
183+
capital: string = ''
184+
}
185+
```
186+
187+
- 인터페이스는 타입을 확장할 수 있으며, 타입은 인터페이스를 확장할 수 있다.
188+
189+
```tsx
190+
interface IStateWithPop extends TState {
191+
population: number
192+
}
193+
194+
type TStateWithPop = IState & { population: number }
195+
196+
// 인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지는 못한다.
197+
// 복잡한 타입을 확장하고 싶다면 타입과 &를 사용해야 한다.
198+
```
199+
200+
- 인터페이스 선언과 타입 선언의 차이점
201+
202+
- 유니온 타입은 있지만 유니온 인터페이스라는 개념은 없다.
203+
- 인터페이스는 타입을 확장할 수 있지만, 유니온은 할 수 없다.
204+
- 유니온 타입을 확장하는 게 필요한 경우
205+
```tsx
206+
type Input = {
207+
/* ... */
208+
}
209+
type Output = {
210+
/* ... */
211+
}
212+
interface VariableMap {
213+
[name: string]: Input | Output
214+
}
215+
```
216+
- 유니온 타입에 name 속성을 붙인 타입도 만들 수 있다.
217+
218+
```tsx
219+
type NamedVariable = (Input | Output) & { name: string }
220+
```
221+
222+
- type 키워드는 유니온이 될 수도 있고, 매핑된 타입 또는 조건부 타입 같은 고급 기능에 활용되기도 한다.
223+
- 튜플과 배열 타입도 type 키워드를 이용해 더 간결하게 표현할 수 있다.
224+
225+
```tsx
226+
type Pair = [number, number]
227+
type StringList = string[]
228+
type NamedNums = [string, ...number[]]
229+
230+
// 인터페이스로 튜플과 비슷하게 구현할 수 있다. 하지만 그러면 튜플에서 사용할 수 있는 concat과 같은 메서드를 사용할 수 없다.
231+
interface Tuple {
232+
0: number
233+
1: number
234+
length: 2
235+
}
236+
```
237+
238+
- 인터페이스는 보강(augment)이 가능하다.
239+
- 선언 병합은 주로 타입 선언 파일에서 사용된다. 따라서 타입 선언 파일을 작성할 때는 선언 병합을 지원하기 위해 반드시 인터페이스를 사용해야 하며 표준을 따라야 한다.
240+
241+
```tsx
242+
// 선언 병합: 속성을 확장하는 것
243+
interface IState {
244+
name: string
245+
capital: string
246+
}
247+
248+
interface IState {
249+
population: number
250+
}
251+
252+
const wyoming: IState = {
253+
name: 'Wyoming',
254+
capital: 'Cheyenne',
255+
population: 500_000,
256+
} // 정상
257+
```
258+
259+
- 타입과 인터페이스 중 어느 것을 사용해야 할까?
260+
261+
- 복잡한 타입이라면 타입 별칭
262+
- 타입과 인터페이스, 두 가지 방법으로 모두 표현할 수 있는 간단한 객체 타입이라면 일관성과 보강의 관점에서 고려해 봐야 한다.
263+
- 향후에 보강의 가능성이 있을지 생각해보고, 어떤 API에 대한 타입 선언을 작성해야 한다면 인터페이스를 사용하는 게 좋다. API가 변경될 때 사용자가 인터페이스를 통해 새로운 필드를 병합할 수 있어 유용하기 때문이다. 그러나 프로젝트 내부적으로 사용되는 타입에 선언 병합이 발생하는 것은 잘못된 설계이다.
264+
265+
<br />
266+
267+
## 아이템 14. 타입 연산과 제너릭 사용으로 반복 줄이기
268+
269+
- 더 큰 집합을 인덱싱해서 속성의 타입에서 중복 제거하기
270+
271+
```tsx
272+
type TopNavState = {
273+
userId: State['userId']
274+
pageTitle: State['pageTitle']
275+
recentFiles: State['recentFiles']
276+
}
277+
```
278+
279+
- ‘매핑된 타입’을 사용해 중복 제거
280+
281+
```tsx
282+
type TopNavState = {
283+
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
284+
}
285+
286+
type Pick<T, K> = {
287+
[k in K]: T[K]
288+
}
289+
290+
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>
291+
```
292+
293+
- 매핑된 타입과 keyof 사용해 선택적 필드로 바꾸기
294+
295+
```tsx
296+
interface Options {
297+
width: number
298+
height: number
299+
color: string
300+
label: string
301+
}
302+
303+
type OptionsUpdate = { [k in keyof Options]?: Options[k] }
304+
305+
/*
306+
interface OptionsUpdate {
307+
width?: number;
308+
height?: number;
309+
color?: string;
310+
label?: string;
311+
}
312+
*/
313+
314+
type OptionsKeys = keyof Options
315+
// 타입이 "width" | "height" | "color" | "label"
316+
317+
type Partial<T> = {
318+
[P in keyof T]?: T[P]
319+
}
320+
```
321+
322+
- 값의 형태에 해당하는 타입을 정의하고 싶을 때
323+
- 값으로부터 타입을 만들어 낼 때는 선언의 순서에 주의해야 한다. 타입 정의를 먼저하고 값이 그 타입에 할당 가능하다고 선언하는 것이 좋다. 그렇게 해야 타입이 더 명확해지고, 예상하기 어려운 타입 변동을 방지할 수 있다.
324+
325+
```tsx
326+
const INIT_OPTIONS = {
327+
width: 640,
328+
height: 480,
329+
color: '#00FF00',
330+
label: 'VGA',
331+
}
332+
333+
type Options = typeof INIT_OPTIONS
334+
335+
/*
336+
interface Options {
337+
width: number;
338+
height: number;
339+
color: string;
340+
label: string;
341+
}
342+
*/
343+
```
344+
345+
- 함수나 메서드의 반환 값에 명명된 타입을 만들고 싶을 때
346+
347+
```tsx
348+
function getUserInfo(userId: string) {
349+
// ...
350+
return {
351+
userId,
352+
name,
353+
// ...
354+
}
355+
}
356+
357+
type UserInfo = ReturnType<typeof getUserInfo>
358+
```
359+
360+
- 제너릭 타입에서 매개변수를 제한할 수 있는 방법
361+
362+
- extends 사용
363+
364+
```tsx
365+
interface Name {
366+
first: string
367+
last: string
368+
}
369+
370+
type DancingDuo<T extends Name> = [T, T]
371+
372+
const couple: DancingDuo<Name> = [
373+
{ first: 'Fred', last: 'Astaire' },
374+
{ first: 'Ginger', last: 'Rogers' },
375+
]
376+
```
377+
378+
- Pick의 정의에서 타입 좁히기
379+
380+
```tsx
381+
type Pick<T, K extends keyof T> = {
382+
[k in K]: T[K]
383+
}
384+
```

0 commit comments

Comments
 (0)