typescript · high priority
TypeScript 심화 — 조건부 · 매핑 · 템플릿 리터럴 · `infer`
타입 레벨 함수형 프로그래밍 — 분배(distributive) · key remapping · 패턴 매칭
학습 개요
탄생 배경
쉬운 설명
복잡한 개념을 실생활 비유로 설명합니다.
“레고 부품 공장의 *조립 라인 설계*”
제네릭은 *부품 모양 일반화*, 조건부 타입은 *분기 컨베이어 (왼/오른쪽으로 보내기)*, 매핑 타입은 *배열의 모든 부품에 같은 작업 적용*, `infer` 는 *지나가는 부품에서 한 조각만 떼어내기*, 템플릿 리터럴은 *부품 라벨 합성기*. 이 다섯 도구가 한 라인에 모이면, *완성품 레고 모양만 보고도* 어떤 조립 라인을 거쳤는지 컴파일러가 *역으로* 계산해낼 수 있습니다. 라이브러리의 "타입 마법" 은 다 이 라인의 조합입니다.
핵심 개념
1// 직접 구현해 본 ReturnType2type MyReturn<F> = F extends (...args: any[]) => infer R ? R : never;34type R1 = MyReturn<() => string>; // string5type R2 = MyReturn<() => Promise<User>>; // Promise<User>67// Parameters 도 같은 패턴8type MyParams<F> = F extends (...args: infer P) => any ? P : never;9type P1 = MyParams<(a: string, b: number) => void>; // [string, number]1011// 중첩 — Promise 안의 타입까지12type Awaited2<T> = T extends Promise<infer U> ? Awaited2<U> : T;13type X = Awaited2<Promise<Promise<string>>>; // string
`infer` 위치가 곧 *어디를 잡을지* 결정
infer 는 *조건부 타입의 extends 절 안* 에서만 쓸 수 있습니다. 위치를 함수 매개변수 자리에 두면 매개변수, 반환 위치에 두면 반환 타입을 잡습니다. *위치 = 의미*.
1type First<T> = T extends [infer F, ...any[]] ? F : never;2type Last<T> = T extends [...any[], infer L] ? L : never;34type A = First<[1, 2, 3]>; // ?5type B = Last<[1, 2, 3]>; // ?6type C = First<[]>; // ?7type D = First<string[]>; // ? (튜플이 아니라 배열)
정답
A = 1, B = 3, C = never, D = string (배열은 길이 불명이지만 첫 요소 타입은 string).
해설
튜플 패턴 매칭은 *고정 길이 + 위치* 가 명확할 때 가장 강력. 빈 튜플은 extends [infer F, ...] 에 매칭되지 않아 never. 일반 배열은 *길이 0 가능성* 때문에 마지막 요소 추출에 더 약하다 (Last<string[]> 도 string 또는 undefined 가 되도록 더 정교하게 짜야 함).
흔한 오답
- 튜플과 배열을 같은 추론으로 가정 — 동작이 다름.
Last<readonly [...]>처럼 readonly 까지 다루려면 별도 분기 필요.
실무 적용
어떤 상황에서 사용하는가
내부 디자인 시스템에서 *컴포넌트 prop 변환 유틸리티* 를 작성한다. `Button` 의 prop 중 일부를 컨테이너가 주입하므로 *주입된 키들을 제외한 나머지만 노출* 해야 하고, 이벤트 핸들러는 *항상 옵셔널* 로 변환되어야 한다.
어떻게 적용하는가
(1) 핵심 유틸리티는 직접 구현 — `Omit<P, InjectedKeys> & Partial<EventProps<P>>` 형태. (2) `EventProps<P>` 는 `as` key remapping + 템플릿 리터럴로 `${string}Click` / `${string}Change` 같은 키만 추출. (3) Omit/Partial 의 분배 동작이 의도와 어긋나는지 *튜플로 감싸* `[K] extends [U]` 형태로 차단할지 검토. (4) DeepPartial 패턴으로 *기본값 머지* 의 타입 안전. (5) `satisfies` 로 컴포넌트 props 객체의 *리터럴 추론* 보존, IDE 자동완성을 살림. (6) 컴파일러 에러가 *Type instantiation is excessively deep* 이면 재귀 종료 조건을 단축하거나 경로 깊이를 *유한* 으로 제한.
흔한 실수와 안티패턴
- 분배 조건부 타입을 모르고 작성 → union 일부가 사라지거나 추가됨.
- `never` 가 분배를 거치면 결과도 `never` — DeepPartial 등에서 빈 결과 반환 버그.
- `as` key remapping 의 `K` 가 `string | number | symbol` — 템플릿 리터럴 직전에 `string & K` 로 좁히지 않으면 에러.
- `--strictFunctionTypes` 와 *메서드 vs 함수 프로퍼티* 의 변성 차이를 잊고 콜백 호환성 디버깅에서 고생.
- 재귀 깊이 한계에 부딪혀 *임의의 깊은 객체* 처리에 실패 — 경로 깊이 제한 필요.
흔한 오해
"`T extends U ? X : Y` 는 그냥 if 처럼 *한 번* 평가된다."
교정*벗겨낸 제네릭* 이 좌측이고 *유니온* 이 들어오면 *각 멤버에 분배* 되어 평가된다. 이걸 막으려면 `[T] extends [U]` 로 감싼다.
왜 중요Exclude/Extract/NonNullable 의 동작 근거가 분배. 모르면 union 한 항이 사라지는 *마법* 으로 보인다.
"`infer` 는 함수에서만 쓴다."
교정튜플 (`[infer F, ...]`), 배열 (`(infer U)[]`), Promise (`Promise<infer R>`), 템플릿 리터럴 (`\`/${infer P}\``) 등 *어떤 패턴* 에서도 쓸 수 있다.
왜 중요`infer` 는 *조건부 타입의 패턴 매칭* 도구라, 매칭 가능한 모든 위치에서 쓸 수 있다.
"매핑 타입의 키는 그대로 유지된다."
교정`as` 절로 *키를 변형* 하거나 `never` 로 보내 *제거* 할 수 있다 (4.1+).
왜 중요getter 자동 생성, 동적 Omit, 이벤트 핸들러 키 추출 등 라이브러리 패턴의 핵심.
면접 질문
답변 방향 힌트
"naked 제네릭", "각 멤버에 분배" 가 키워드.
반드시 언급할 키워드
- `Exclude<T, U> = T extends U ? never : T`
- *벗겨낸* 제네릭 좌측 + *유니온* 입력 → 분배 발동
- 각 멤버에 평가: `"a" extends "a" ? never : "a"` = never, 나머지는 자기 자신
- union 결과: `never | "b" | "c"` = `"b" | "c"`
- `never` 가 결과에서 흡수됨
- 분배를 막으려면 `[T] extends [U]`
예상 꼬리 질문
- `ToArray<never>` 가 왜 `never` 가 되는지 같은 메커니즘으로 설명해 주세요.
- `Exclude` 와 `Extract` 의 *차이가 한 글자* 인 이유는?
자기 점검
*벗겨낸 제네릭* 이 무엇이며, 분배가 발동하는 *두 조건* 은?
기대 키워드
자주 하는 오해
"조건부 타입은 항상 분배된다" — naked 가 아니거나 union 이 아니면 분배 안 됨.
`as` key remapping 의 `as` 분기에서 `never` 를 반환하면 어떤 일이 벌어지나?
기대 키워드
자주 하는 오해
"키가 never 라는 이름으로 남는다" — 아예 *결과 객체에서 사라진다*.
`(x: Animal) => void` 와 `(x: Dog) => void` 중 어느 쪽이 어느 쪽에 *대입 가능* 한가? 변성 단어로 답하라.
기대 키워드
자주 하는 오해
"Dog 가 더 구체적이니 Dog 함수가 어디든 들어간다" — 매개변수는 *반공변* 이라 반대다.
학습 자료
- TypeScript Handbook — Conditional Types`infer` · 분배 · 재귀까지 공식 가이드.Doctypescriptlang.org
- TypeScript Handbook — Mapped Typesmodifier · `as` key remapping · 매핑 패턴.Doctypescriptlang.org
- TypeScript Handbook — Template Literal Types문자열 합성·분해 + 내장 string manipulation 4종.Doctypescriptlang.org
- TypeScript 2.8 Release — Conditional Types조건부 타입과 `infer` 가 처음 들어온 릴리즈 노트.Doctypescriptlang.org
- type-challenges조건부·매핑·infer 를 직접 구현하며 익히는 연습 모음.Codetype-challenges