|
| 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