본문 바로가기
C.W.K.
Stream
Lesson 05 of 05 · published

분배 조건부 타입: 유니언이 항목별로 계산될 때

~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
"조건부 타입의 입력이 유니언이면, TypeScript는 종종 전체를 한 번 묻지 않고 각 항목에 같은 질문을 따로 던져."

분배가 일어나는 조건

조건부 타입이 T extends X ? A : B이고 검사하는 T가 가공되지 않은 제네릭 매개변수라면, T에 유니언이 들어왔을 때 각 구성원에 조건을 따로 적용해. 결과는 각 계산 결과의 유니언으로 다시 합쳐진다.

type Wrap<T> = T extends unknown ? { value: T } : never에 string 또는 number를 넣으면 { value: string } | { value: number }가 돼. { value: string | number } 하나와는 다른 모양이야.

유니언 필터가 되는 이유

Exclude<T, U>는 T의 각 구성원을 U에 대입할 수 있는지 검사하고, 맞으면 never, 아니면 원래 구성원을 돌려줘. 유니언에서 never는 사라지므로 원하는 항목만 남는다. NonNullable 같은 표준 도구도 같은 분배 성질을 이용해.

전체를 한 번에 검사하고 싶을 때

분배를 막으려면 양쪽을 한 칸짜리 튜플로 감싸 [T] extends [X]라고 써. 이제 검사 대상이 가공되지 않은 T가 아니라 튜플이므로 전체 유니언을 한 덩어리로 비교해. ‘각 항목이 배열인가?’와 ‘전체 유니언이 배열에 대입 가능한가?’는 다른 질문이야.

왜 결과가 이상해 보이는가

고급 타입에서 예상보다 유니언이 많이 생기면 먼저 분배가 일어났는지 확인해. 반대로 항목별 변환을 기대했는데 결과가 하나라면 T가 객체나 배열 안에 감싸져 분배 조건을 잃었을 수 있어. 규칙을 외우기보다 컴파일러가 전체에 물었는지 각 항목에 물었는지 구별하면 돼.

항목별 계산에는 그대로 둔 T, 전체 판단에는 [T]를 써. 대괄호 한 쌍이 질문의 단위를 바꾼다.

피파의 고백

분배를 처음 만나면 컴파일러가 마음대로 타입을 찢은 것처럼 보여. 실제로는 내가 ‘각 T에 대해’라고 적어 놓고 전체 질문을 기대했던 거야. 타입 오류가 이상할수록 문법보다 질문의 단위를 먼저 확인해.

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>를 직접 구현해 string | null | undefined를 string으로 줄여. 이어서 WrapAll<T>string | number{ value: string } | { value: number }로 바꾸고, 에디터에서 두 결과 타입을 확인해.
Hint
분배 조건부 타입은 입력 유니언의 각 멤버를 별도로 검사해. 조건을 통과하지 못해 never가 된 멤버는 최종 유니언에서 제거돼.

Progress

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

댓글 0

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

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