|
| 1 | +## 아이템 15. 동적 데이터에 인덱스 시그니처 사용하기 |
| 2 | + |
| 3 | +- 인덱스 시그니처 |
| 4 | + - 동적 데이터를 표현할 때 사용 |
| 5 | + ```tsx |
| 6 | + type Rocket = { [property: string]: string } |
| 7 | + const rocket: Rocket = { |
| 8 | + name: 'Falcon 9', |
| 9 | + variant: 'v1.0', |
| 10 | + thrust: '4,940 kN', |
| 11 | + } // 정상 |
| 12 | + ``` |
| 13 | + - 키의 이름 `property` |
| 14 | + - 키의 위치만 표시하는 용도. 타입 체커에서는 사용하지 않음. |
| 15 | + - 키의 타입 `string` |
| 16 | + - string이나 number 또는 symbol의 조합이어야 하지만, 보통은 string을 사용. |
| 17 | + - 값의 타입 `string` |
| 18 | + - 어떤 것이든 될 수 있음. |
| 19 | +- 인덱스 시그니처의 4가지 단점 |
| 20 | + - 잘못된 키를 포함해 모든 키를 허용한다. 오타를 잡을 수 없다. |
| 21 | + - 객체 안에 프로퍼티가 하나도 없는 {}도 유효한 타입이 된다. |
| 22 | + - 키마다 다른 타입을 가질 수 없고, 키들의 타입이 통일되어야 한다. |
| 23 | + - 키를 입력할 때 키에는 어떤 값도 들어올 수 있으므로 자동 완성 기능이 동작하지 않는다. |
| 24 | +- 인덱스 시그니처 대신 사용 가능한 2가지 대안 |
| 25 | + - `Record<K, T>` |
| 26 | + - 키 타입에 유연성을 제공하는 제너릭 타입 |
| 27 | + ```tsx |
| 28 | + type Vec3D = Record<'x' | 'y' | 'z', number> |
| 29 | + // type Vec3D = Record<키에 들어올 수 있는 타입, 값의 타입>; |
| 30 | + // Type Vec3D = { |
| 31 | + // x: number; |
| 32 | + // y: number; |
| 33 | + // z: number; |
| 34 | + // } |
| 35 | + ``` |
| 36 | + - 인덱스 시그니처의 파라미터 타입은 리터럴이 될 수 없다. |
| 37 | + ```tsx |
| 38 | + type fruitInfo = { |
| 39 | + [fruit: '사과' | '딸기' | '포도']: number |
| 40 | + } |
| 41 | + // An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead. |
| 42 | + ``` |
| 43 | + - 매핑된 타입 사용 |
| 44 | + - 키마다 별도의 타입을 사용하게 해준다. |
| 45 | + ```tsx |
| 46 | + type ABC = { [k in 'a' | 'b' | 'c']: k extends 'b' ? string : number } |
| 47 | + // Type ABC = { |
| 48 | + // a: number; |
| 49 | + // b: string; |
| 50 | + // c: number; |
| 51 | + // } |
| 52 | + ``` |
| 53 | +- 런타임 때까지 객체의 속성을 알 수 없을 경우에만(예를 들어 CSV 파일에서 로드하는 경우) 인덱스 시그니처를 사용하도록 한다. |
| 54 | +- 안전한 접근을 위해 인덱스 시그니처의 값 타입에 undefined를 추가하는 것을 고려해야 한다. |
| 55 | +- 어떤 타입에 가능한 필드가 제한되어 있는 경우라면 인덱스 시그니처로 모델링하지 말아야 한다. |
| 56 | + ```tsx |
| 57 | + interface Row1 { |
| 58 | + [column: string]: number |
| 59 | + } // 너무 광범위 |
| 60 | + interface Row2 { |
| 61 | + a: number |
| 62 | + b?: number |
| 63 | + c?: number |
| 64 | + d?: number |
| 65 | + } // 최선 |
| 66 | + type Row3 = |
| 67 | + | { a: number } |
| 68 | + | { a: number; b: number } |
| 69 | + | { a: number; b: number; c: number } |
| 70 | + | { a: number; b: number; c: number; d: number } // 가장 정확하지만 사용하기 번거로움 |
| 71 | + ``` |
| 72 | +- 연관 배열(associative array)의 경우, 객체에 인덱스 시그니처를 사용하는 대신 Map 타입을 사용하는 것을 고려할 수 있다. ← 잘 이해는 안되지만 298페이지에 관련 내용 나온다고 함. |
| 73 | +
|
| 74 | +<br /> |
| 75 | +
|
| 76 | +## 아이템 16. number 인덱스 시그니처보다는 Array, 튜플, ArrayLike를 사용하기 |
| 77 | +
|
| 78 | +- 자바스크립트의 이상한 동작(feat. 암시적 타입 강제) |
| 79 | + - 배열은 객체이므로 키는 숫자가 아니라 문자열이다. |
| 80 | + - 자바스크립트에서는 숫자나 복잡한 객체를 키로 사용하려고 하면 문자열로 변환해 버린다. |
| 81 | + - 배열에서 특정 요소를 접근할 때 인덱스를 문자열로 적어도 가능. 배열의 키를 Object.keys로 나열해보면 문자열로 출력된다. |
| 82 | +- 타입스크립트는 암시적 타입 강제의 혼란을 바로잡기 위해 숫자 키를 허용하고, 문자열 키와 다른 것으로 인식한다. (런타입에서는 문자열 키로 인식) |
| 83 | +- 인덱스를 신경 쓰지 않는다면, 배열을 순회할 때 `for...in` 보다 `for...of` 를 써라. 인덱스가 중요하다면 `forEach` 를 써라. |
| 84 | +
|
| 85 | + ```tsx |
| 86 | + const xs = [1, 2, 3] |
| 87 | + |
| 88 | + const keys = Object.keys(xs) // 타입이 stirng[] |
| 89 | + for (const key in xs) { |
| 90 | + key // 타입이 string |
| 91 | + const x = xs[key] // 타입이 number |
| 92 | + } |
| 93 | + |
| 94 | + for (const x of xs) { |
| 95 | + x // 타입이 number |
| 96 | + } |
| 97 | + |
| 98 | + xs.forEach((x, i) => { |
| 99 | + i // 타입이 number |
| 100 | + x // 타입이 number |
| 101 | + }) |
| 102 | + ``` |
| 103 | + |
| 104 | + - `for...in` 은 `for...of` 나 `for` 루프보다 몇 배나 느리다. |
| 105 | + |
| 106 | +- 인덱스 시그니처에 number를 사용하기 보다 Array, 튜플, ArrayLike 타입을 사용하는 것이 좋다. |
| 107 | +- 어떤 길이를 가지는 배열과 비슷한 형태의 튜플을 사용하고 싶다면 타입스크립트에 있는 ArrayLike 타입을 사용한다. |
| 108 | + ```tsx |
| 109 | + const tupleLike: ArrayLike<string> = { |
| 110 | + '0': 'A', |
| 111 | + '1': 'B', |
| 112 | + length: 2, |
| 113 | + } |
| 114 | + ``` |
| 115 | + |
| 116 | +<br /> |
| 117 | + |
| 118 | +## 아이템 17. 변경 관련된 오류 방지를 위해 readonly 사용하기 |
| 119 | + |
| 120 | +- `readonly number[]` 와 `number[]` 가 구분되는 특징 |
| 121 | + - 배열의 요소를 읽을 수 있지만, 쓸 수는 없다. |
| 122 | + - length를 읽을 수 있지만, 바꿀 수는 없다. |
| 123 | + - 배열을 변경하는 pop을 비롯한 다른 메서드를 호출할 수 없다. |
| 124 | + - `number[]` 는 `readonly number[]` 보다 기능이 많기 때문에 `readonly number[]` 의 서브 타입이 된다. |
| 125 | +- 매개변수를 readonly로 선언하면 아래와 같은 일이 생긴다. |
| 126 | + - 타입스크립트는 매개변수가 함수 내에서 변경이 일어나는지 체크한다. |
| 127 | + - 호출하는 쪽에서는 함수가 매개변수를 변경하지 않는다는 보장을 받게 된다. |
| 128 | + - 호출하는 쪽에서 함수에 readonly 배열을 매개변수로 넣을 수도 있다. |
| 129 | +- 매개변수를 readonly로 선언할 때의 장점 |
| 130 | + - 더 넓은 타입으로 호출할 수 있고, 의도치 않은 변경은 방지될 것이다. |
| 131 | + - 지역 변수와 관련된 모든 종류의 변경 오류를 방지할 수 있고, 변경이 발생하는 코드도 쉽게 찾을 수 있다. |
| 132 | + - 함수가 매개변수를 수정하지 않는다면 readonly로 선언하는 것이 좋다. readonly 매개변수는 인터페이스를 명확하게 하며, 매개변수가 변경되는 것을 방지한다. |
| 133 | +- 단점? |
| 134 | + - 어떤 함수를 readonly로 만들면 그 함수를 호출하는 다른 함수도 모두 readonly로 만들어야 한다. 다른 라이브러리에 있는 함수를 호출하는 경우라면 타입 선언을 바꿀 수 없기 때문에 타입 단언문을 사용해야 한다. |
| 135 | +- readonly, Readonly 제너릭은 얕게 동작한다는 것에 유의하며 사용해야 한다. |
| 136 | + |
| 137 | + ```tsx |
| 138 | + interface Outer { |
| 139 | + inner: { |
| 140 | + x: number |
| 141 | + } |
| 142 | + } |
| 143 | + |
| 144 | + const o: Readonly<Outer> = { inner: { x: 0 } } |
| 145 | + o.inner = { x: 1 } |
| 146 | + // Cannot assign to 'inner' because it is a read-only property. |
| 147 | + o.inner.x = 1 |
| 148 | + |
| 149 | + type T = Readonly<Outer> |
| 150 | + // type T = { |
| 151 | + // readonly inner: { |
| 152 | + // x: number; |
| 153 | + // }; |
| 154 | + // } |
| 155 | + ``` |
| 156 | +
|
| 157 | +- 깊은 readonly 타입을 쓰고 싶으면 ts-essentials에 있는 DeepReadonly 제너릭을 사용하면 된다. |
| 158 | +- 인덱스 시그니처에도 readonly를 쓸 수 있다. 일기는 허용하되 쓰기를 방지하는 효과가 있다. |
| 159 | + ```tsx |
| 160 | + let obj: { readonly [k: string]: number } = {} |
| 161 | + // 또는 Readonly<{[k: string]: number}> |
| 162 | + obj.hi = 45 |
| 163 | + // Index signature in type '{ readonly [k: string]: number; }' only permits reading. |
| 164 | + obj = { ...obj, hi: 12 } |
| 165 | + ``` |
| 166 | + |
| 167 | +<br /> |
| 168 | + |
| 169 | +## 아이템 18. 매핑된 타입을 사용하여 값을 동기화하기 |
| 170 | + |
| 171 | +- 어떤 인터페이스에 속성이 추가될 때마다 다른 인터페이스의 속성도 같이 추가되어야 한다고 할 때 매핑된 타입을 사용해 값을 동기화하는 방식을 쓰는 것이 좋다. |
| 172 | + |
| 173 | +```tsx |
| 174 | +interface ScatterProps { |
| 175 | + xs: number[] |
| 176 | + ys: number[] |
| 177 | + color: string |
| 178 | + onClick: () => void |
| 179 | + onDoubleClick: () => void |
| 180 | +} |
| 181 | + |
| 182 | +const REQUIRES_UPDATE: { [k in keyof ScatterProps]: boolean } = { |
| 183 | + xs: true, |
| 184 | + ys: true, |
| 185 | + color: true, |
| 186 | + onClick: false, |
| 187 | +} |
| 188 | +// Property 'onDoubleClick' is missing in type '{ xs: true; ys: true; color: true; onClick: false; }' but required in type '{ xs: boolean; ys: boolean; color: boolean; onClick: boolean; onDoubleClick: boolean; }'. |
| 189 | +// onDoubleClick 속성을 추가하라고 에러가 뜬다. |
| 190 | +``` |
0 commit comments