|
| 1 | +--- |
| 2 | +title: Conditional Types |
| 3 | +layout: docs |
| 4 | +permalink: /ko/docs/handbook/2/conditional-types.html |
| 5 | +oneline: "타입 시스템에서 if문 처럼 동작하는 타입 생성하기." |
| 6 | +--- |
| 7 | + |
| 8 | +대부분 유용한 프로그램의 핵심은, 입력에 따라 출력이 결정되어야 한다는 것입니다. |
| 9 | +JavaScript 프로그램도 크게 다르진 않지만, 값의 타입을 쉽게 검사할 수 있다는 사실을 고려할 때, 출력에 대한 결정은 또한 입력의 타입에도 기반합니다. |
| 10 | +_조건부 타입_ 은 입력과 출력 타입간의 관계를 설명하는 데 도움을 줄 수 있습니다. |
| 11 | + |
| 12 | +```ts twoslash |
| 13 | +interface Animal { |
| 14 | + live(): void; |
| 15 | +} |
| 16 | +interface Dog extends Animal { |
| 17 | + woof(): void; |
| 18 | +} |
| 19 | + |
| 20 | +type Example1 = Dog extends Animal ? number : string; |
| 21 | +// ^? |
| 22 | + |
| 23 | +type Example2 = RegExp extends Animal ? number : string; |
| 24 | +// ^? |
| 25 | +``` |
| 26 | + |
| 27 | +조건부 타입은 JavaScript에 있는 삼항 연산자 조건문 (`condition ? trueExpression : falseExpression`) 같은 형태를 가집니다. |
| 28 | + |
| 29 | +```ts twoslash |
| 30 | +type SomeType = any; |
| 31 | +type OtherType = any; |
| 32 | +type TrueType = any; |
| 33 | +type FalseType = any; |
| 34 | +type Stuff = |
| 35 | + // ---cut--- |
| 36 | + SomeType extends OtherType ? TrueType : FalseType; |
| 37 | +``` |
| 38 | + |
| 39 | +`extends`를 기준으로 왼쪽에 있는 타입이 오른쪽 타입에 할당할 수 있다면 첫 번째 분기("참"값 분기)를, 그렇지 않다면 뒤의 분기("거짓"값 분기)를 얻게 됩니다. |
| 40 | + |
| 41 | +`Dog extends Animal` 에 따라 `number`나 `string`인지 알려주는 것 말곤, 위의 예제에서 조건부 타입은 그다지 유용해 보이지 않습니다! |
| 42 | +하지만 제네릭과 함께 사용될 때 조건부 타입은 강력한 힘을 갖습니다. |
| 43 | + |
| 44 | +예를 들어, 다음 `createLabel` 함수를 살펴보겠습니다. |
| 45 | + |
| 46 | +```ts twoslash |
| 47 | +interface IdLabel { |
| 48 | + id: number /* some fields */; |
| 49 | +} |
| 50 | +interface NameLabel { |
| 51 | + name: string /* other fields */; |
| 52 | +} |
| 53 | + |
| 54 | +function createLabel(id: number): IdLabel; |
| 55 | +function createLabel(name: string): NameLabel; |
| 56 | +function createLabel(nameOrId: string | number): IdLabel | NameLabel; |
| 57 | +function createLabel(nameOrId: string | number): IdLabel | NameLabel { |
| 58 | + throw "unimplemented"; |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +createLabel의 오버로드들은 입력 타입에 따른 단일 JavaScript 함수를 나타냅니다. 다음을 주목하세요. |
| 63 | + |
| 64 | +1. 만약 라이브러리가 매번 API 전체에서 비슷한 종류의 함수를 만들어야 한다면 번거로워집니다. |
| 65 | +2. 우린 3가지 오버로드 즉, 각 케이스별로 _확실한_ 타입을 가지거나 (각각 `number`와 `string`) 그리고 일반적인 케이스(`string | number`) 가져야 합니다. `createLabel`의 새로운 타입을 다루기 위해선 오버로드의 수는 기하급수적으로 증가합니다. |
| 66 | + |
| 67 | +대신에 조건부 타입으로 로직을 인코딩할 수 있습니다. |
| 68 | + |
| 69 | +```ts twoslash |
| 70 | +interface IdLabel { |
| 71 | + id: number /* some fields */; |
| 72 | +} |
| 73 | +interface NameLabel { |
| 74 | + name: string /* other fields */; |
| 75 | +} |
| 76 | +// ---cut--- |
| 77 | +type NameOrId<T extends number | string> = T extends number |
| 78 | + ? IdLabel |
| 79 | + : NameLabel; |
| 80 | +``` |
| 81 | + |
| 82 | +조건부 타입을 사용하면 단일 함수까지 오버로드 없이 단순화 시킬 수 있습니다. |
| 83 | + |
| 84 | +```ts twoslash |
| 85 | +interface IdLabel { |
| 86 | + id: number /* some fields */; |
| 87 | +} |
| 88 | +interface NameLabel { |
| 89 | + name: string /* other fields */; |
| 90 | +} |
| 91 | +type NameOrId<T extends number | string> = T extends number |
| 92 | + ? IdLabel |
| 93 | + : NameLabel; |
| 94 | +// ---cut--- |
| 95 | +function createLabel<T extends number | string>(idOrName: T): NameOrId<T> { |
| 96 | + throw "unimplemented"; |
| 97 | +} |
| 98 | + |
| 99 | +let a = createLabel("typescript"); |
| 100 | +// ^? |
| 101 | + |
| 102 | +let b = createLabel(2.8); |
| 103 | +// ^? |
| 104 | + |
| 105 | +let c = createLabel(Math.random() ? "hello" : 42); |
| 106 | +// ^? |
| 107 | +``` |
| 108 | + |
| 109 | +### 조건부 타입으로 제한하기 |
| 110 | + |
| 111 | +종종, 조건부 타입의 검사에서 새로운 정보를 얻을 수 있습니다. |
| 112 | +타입 가드가 더 구체적인 타입으로 좁혀주듯이, 조건부 타입의 "참"값 분기는 대조하는 타입에 따라서 제네릭을 더 제한할 수 있습니다. |
| 113 | + |
| 114 | +다음 예를 살펴보겠습니다. |
| 115 | + |
| 116 | +```ts twoslash |
| 117 | +// @errors: 2536 |
| 118 | +type MessageOf<T> = T["message"]; |
| 119 | +``` |
| 120 | + |
| 121 | +위 예제에서, `T`가 `message` 프로퍼티를 가지고 있는지 알 수 없기 때문에 TypeScript에서 오류가 발생합니다. |
| 122 | +`T`의 타입을 제한해서 TypeScript가 더이상 오류를 내지 않도록 만들 수 있습니다. |
| 123 | + |
| 124 | +```ts twoslash |
| 125 | +type MessageOf<T extends { message: unknown }> = T["message"]; |
| 126 | + |
| 127 | +interface Email { |
| 128 | + message: string; |
| 129 | +} |
| 130 | + |
| 131 | +type EmailMessageContents = MessageOf<Email>; |
| 132 | +// ^? |
| 133 | +``` |
| 134 | + |
| 135 | +하지만 `MessageOf` 가 아무 타입이나 받을 수 있고, `message` 프로퍼티가 없으면 `never` 타입으로 결정하도록 만들 수 있을까요? |
| 136 | +여기서 제약 조건을 외부로 옮기고, 조건부 타입을 적용하면 가능합니다. |
| 137 | + |
| 138 | +```ts twoslash |
| 139 | +type MessageOf<T> = T extends { message: unknown } ? T["message"] : never; |
| 140 | + |
| 141 | +interface Email { |
| 142 | + message: string; |
| 143 | +} |
| 144 | + |
| 145 | +interface Dog { |
| 146 | + bark(): void; |
| 147 | +} |
| 148 | + |
| 149 | +type EmailMessageContents = MessageOf<Email>; |
| 150 | +// ^? |
| 151 | + |
| 152 | +type DogMessageContents = MessageOf<Dog>; |
| 153 | +// ^? |
| 154 | +``` |
| 155 | + |
| 156 | +"참"값 분기내에서는 TypeScript는 `T`가 `message` 프로퍼티를 가지고 _있을 것을_ 알 수 있습니다. |
| 157 | + |
| 158 | +또 다른 예제에서 배열 타입이면 배열의 개별 요소 타입으로 평탄화 시키지만, 배열 타입이 아니면 그대로 유지하는 `Flatten` 타입을 만들 수 있습니다. |
| 159 | + |
| 160 | +```ts twoslash |
| 161 | +type Flatten<T> = T extends any[] ? T[number] : T; |
| 162 | + |
| 163 | +// Extracts out the element type. |
| 164 | +type Str = Flatten<string[]>; |
| 165 | +// ^? |
| 166 | + |
| 167 | +// Leaves the type alone. |
| 168 | +type Num = Flatten<number>; |
| 169 | +// ^? |
| 170 | +``` |
| 171 | + |
| 172 | +`Flatten`에 배열 타입이 주어지면, `number`를 사용한 인덱스 접근을 통해 `string[]`의 요소 타입을 가져올 수 있습니다. |
| 173 | +그렇지 않으면, 주어진 타입을 반환합니다. |
| 174 | + |
| 175 | +### 조건부 타입 내에서 추론하기 |
| 176 | + |
| 177 | +위에서 제약 조건을 가진 조건부 타입을 이용해서 타입을 추출할 수 있다는 점을 살펴봤습니다. |
| 178 | +이 부분은 조건부 타입을 더 쉽게 만드는 평범한 작업이 됩니다. |
| 179 | + |
| 180 | +조건부 타입은 `infer` 키워드를 사용해서 "참"값 분기에서 비교하는 타입을 추론할 수 있습니다. |
| 181 | +예를 들어, `Flatten`에서 인덱싱된 접근 타입으로 "직접" 추출하지 않고 요소 타입을 추론할 수 있습니다. |
| 182 | + |
| 183 | +```ts twoslash |
| 184 | +type Flatten<Type> = Type extends Array<infer Item> ? Item : Type; |
| 185 | +``` |
| 186 | + |
| 187 | +여기 "참"값 분기에서 `T`의 요소 타입을 어떻게 제시할 필요 없이, `infer` 키워드를 새 제네릭 타입 변수 `Item`에 선언적으로 사용했습니다. |
| 188 | +이 방식은 관심 있는 타입의 구조를 깊게 분석하지 않아도 되도록 만들어줍니다. |
| 189 | + |
| 190 | +`infer` 키워드를 사용해서 유용한 헬퍼 타입 별칭을 사용할 수 있습니다. |
| 191 | +예를 들어 함수 타입에서 리턴 타입을 추출하는 간단한 케이스를 살펴보겠습니다. |
| 192 | + |
| 193 | +```ts twoslash |
| 194 | +type GetReturnType<Type> = Type extends (...args: never[]) => infer Return |
| 195 | + ? Return |
| 196 | + : never; |
| 197 | + |
| 198 | +type Num = GetReturnType<() => number>; |
| 199 | +// ^? |
| 200 | + |
| 201 | +type Str = GetReturnType<(x: string) => string>; |
| 202 | +// ^? |
| 203 | + |
| 204 | +type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; |
| 205 | +// ^? |
| 206 | +``` |
| 207 | + |
| 208 | +여러 호출 시그니처 (오버로트 함수 타입 같이)를 가진 타입을 추론할 때, _마지막_ 시그니처 (아마, 모든 케이스에 허용되는)로 추론하게 됩니다. 인자 타입의 목록에 기반해서 오버로드들을 처리할 수는 없습니다. |
| 209 | + |
| 210 | +```ts twoslash |
| 211 | +declare function stringOrNum(x: string): number; |
| 212 | +declare function stringOrNum(x: number): string; |
| 213 | +declare function stringOrNum(x: string | number): string | number; |
| 214 | + |
| 215 | +type T1 = ReturnType<typeof stringOrNum>; |
| 216 | +// ^? |
| 217 | +``` |
| 218 | + |
| 219 | +## 분산적인 조건부 타입 |
| 220 | + |
| 221 | +제네릭 타입 위에서 조건부 타입은 유니언 타입을 만나면 _분산적으로_ 동작합니다. |
| 222 | +예를 들어 다음을 보겠습니다. |
| 223 | + |
| 224 | +```ts twoslash |
| 225 | +type ToArray<Type> = Type extends any ? Type[] : never; |
| 226 | +``` |
| 227 | + |
| 228 | +`ToArray`에 유니언 타입을 넘기면 조건부 타입은 유니언의 각 멤버에 적용됩니다. |
| 229 | + |
| 230 | +```ts twoslash |
| 231 | +type ToArray<Type> = Type extends any ? Type[] : never; |
| 232 | + |
| 233 | +type StrArrOrNumArr = ToArray<string | number>; |
| 234 | +// ^? |
| 235 | +``` |
| 236 | + |
| 237 | +`StrArrOrNumArr`이 동작하는 방식은 다음과 같습니다. |
| 238 | + |
| 239 | +```ts twoslash |
| 240 | +type StrArrOrNumArr = |
| 241 | + // ---cut--- |
| 242 | + string | number; |
| 243 | +``` |
| 244 | + |
| 245 | +유니언의 각 멤버 타입은 효율적으로 매핑됩니다. |
| 246 | + |
| 247 | +```ts twoslash |
| 248 | +type ToArray<Type> = Type extends any ? Type[] : never; |
| 249 | +type StrArrOrNumArr = |
| 250 | + // ---cut--- |
| 251 | + ToArray<string> | ToArray<number>; |
| 252 | +``` |
| 253 | + |
| 254 | +그리고 다음과 같이 결과가 나옵니다. |
| 255 | + |
| 256 | +```ts twoslash |
| 257 | +type StrArrOrNumArr = |
| 258 | + // ---cut--- |
| 259 | + string[] | number[]; |
| 260 | +``` |
| 261 | + |
| 262 | +일반적으로 분산성이 원하는 동작입니다. 이러한 동작을 방지하려면 `extends`키워드의 양 옆을 대괄호로 감싸면 됩니다. |
| 263 | + |
| 264 | +```ts twoslash |
| 265 | +type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never; |
| 266 | + |
| 267 | +// 'StrArrOrNumArr' is no longer a union. |
| 268 | +type StrArrOrNumArr = ToArrayNonDist<string | number>; |
| 269 | +// ^? |
| 270 | +``` |
0 commit comments