C.W.K.
Stream
Lesson 05 of 05 · published

Distributive Union: `T | U` 가 이상해질 때

~10 min · unions-intersections, distribution, conditional-types, generics

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"Conditional 타입이 union 에 분배. 알게 되면 몇몇 신비로운 타입 에러가 신비롭지 않아져."

규칙

Conditional 타입의 체크 타입이 벗겨진 (naked) generic 타입 parameter 이고, 그 parameter 가 union 이면, TypeScript 가 conditional 을 union 의 각 멤버에 분배하고 결과를 union. 기호로: T = U1 | U2 에 적용된 T extends X ? A : B(U1 extends X ? A : B) | (U2 extends X ? A : B) 됨.

보통 원하는 거. 가끔 아냐. Escape hatch: parameter 를 bracket 으로 감쌈 — [T] extends [X] — 비교를 single-element tuple test 로 변환하고 분배 비활성화.

분배가 도움될 때

고전 사용: union 에 대한 타입-레벨 mapping. type Wrap<T> = T extends unknown ? { value: T } : never. `string | number` 에 적용하면 분배: Wrap<string> | Wrap<number> = { value: string } | { value: number }. 입력의 각 멤버가 출력에 자기 wrap 된 variant 됨.

같은 패턴: standard library 의 `Exclude` 가 T extends U ? never : T 로 구현 — 분배가 union 에서 특정 멤버 제거하게 만드는 거.

분배가 surprise 일 때

단일 결과 기대하고 union 받음. 또는 분배 기대했는데 안 받음 — 타입 parameter 가 naked 가 아니라서 (다른 모양에 wrap). 두 방향 fix:

  • 분배 강제 하려면: conditional 체크 site 에서 parameter naked 유지.
  • 분배 막으려면: tuple 로 감쌈 — [T] extends [X].

Bracket 트릭이 많은 고급 TypeScript 패턴에 나타나. 존재 알면, library 타입 정의에서 항상 발견해.

실용 규칙: Generic conditional 타입이 이상하게-분배된 결과 주면, 체크를 bracket 으로 감싸고 그게 원한 거였는지 봐. Yes 면 ship. No 면 분배가 옳았어 — 결과 타입 신중히 읽고 어느 입력 variant 가 어느 출력 만들었는지 알아내.

피파의 고백

cwkPippa 에서 bracket 트릭에 한 4번 손 뻗었어 — 각각 generic 타입에 대해 union 멤버에 분배된 답 말고 단일 yes/no 답 원했을 때. 일상 사용 기능 아닌데, 필요할 땐 다른 어느 것도 일 안 해. 드물게만 써도 존재 알 가치 있어.

Code

기능으로서의 분배·typescript
// 분배 in action — Wrap 예시.
type Wrap<T> = T extends unknown ? { value: T } : never;

type A = Wrap<string>;                    // { value: string }
type B = Wrap<string | number>;
// 분배:
//   Wrap<string> | Wrap<number>
//   { value: string } | { value: number }

// Exclude<T, U> — standard library 에 내장.
type WithoutNull<T> = Exclude<T, null>;
type C = WithoutNull<string | null>;       // string

// 분배 없으면 Exclude 가 작동 못 — per-member test 에 의존.
Bracket 트릭 — 분배 비활성화·typescript
// 분배가 원하는 게 아닐 때.

// Bracket 없이 — 분배.
type IsExactlyArray<T> = T extends any[] ? true : false;
type X1 = IsExactlyArray<string | string[]>;
//   IsExactlyArray<string> | IsExactlyArray<string[]>
//   false | true
// 단일 yes/no 원했지, 두 union 원한 거 아냐.

// Bracket 과 함께 — 분배 비활성화.
type IsExactlyArray2<T> = [T] extends [any[]] ? true : false;
type X2 = IsExactlyArray2<string | string[]>;
//   [string | string[]] extends [any[]] ? true : false
//   false   (string | string[] 이 any[] 에 assignable 아니라서)

// Bracket 이 테스트를 '이 전체 타입 assignable 한가' 로 변환.

External links

Exercise

NonNullable<T> utility 직접 써: string | null | undefinedstring 으로 바꾸기. 분배 써. 그다음 string | number{ value: string } | { value: number } 로 바꾸는 WrapAll<T> 써. 에디터에서 hover 해서 결과 타입 확인.
Hint
type NonNullable<T> = T extends null | undefined ? never : T. 분배가 입력의 각 멤버 별도 test, never 멤버가 결과 union 에서 제거됨. WrapAll 은 더 단순: type WrapAll<T> = T extends unknown ? { value: T } : never.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.