숏컷 코드
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;조건부 타입
conditional type에서 먼저 자주 보는 기본형은 아래입니다.
- 단순 분기:
T extends U ? X : Y - 함수 반환 타입 추출:
T extends (...args: any[]) => infer R ? R : never - 배열 원소 타입 추출:
T extends (infer U)[] ? U : never - Promise 안쪽 타입 추출:
T extends Promise<infer P> ? P : T
1) 타입 수준의 if 문이 필요하면 conditional type
T extends U ? X : Y는 값이 아니라 타입에 대해 분기하는 문법입니다.
type IsString<T> = T extends string ? true : false;즉 제네릭 입력에 따라 결과 타입을 달리 만들고 싶을 때 핵심이 됩니다.
2) 내부 타입 일부를 꺼내면 infer
함수 반환 타입, 배열 원소 타입 같은 내부 조각을 패턴 매칭처럼 추출할 때 infer를 씁니다.
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type ElementOf<T> = T extends (infer U)[] ? U : never;이 패턴 덕분에 복잡한 타입 구조를 다시 손으로 쓰지 않고 필요한 부분만 뽑을 수 있습니다.
type User = { id: string };
type UserList = User[];
type UserItem = ElementOf<UserList>; // User3) utility type의 내부 원리를 읽는 카드이기도 하다
이 문법은 앱 코드보다 유틸리티 타입이나 라이브러리 설계에서 특히 자주 보입니다. 즉 "직접 많이 쓴다"보다 "이런 타입이 어떻게 돌아가는지 읽는다"는 관점도 중요합니다.
4) 간단한 문제엔 일반 타입 별칭이 더 낫다
conditional type은 강력하지만, 단순한 경우까지 무조건 이 방식으로 풀면 선언과 오류 메시지가 빠르게 어려워집니다.
type Id = string | number;
// 이런 경우는 conditional type보다 직접 별칭이 더 읽기 쉽다.
type UserId = Id;5) 중첩이 늘어날수록 가독성을 같이 본다
고급 타입 조작은 가능하다고 다 쓰는 것이 아니라, 독자가 디버깅할 수 있는 수준인지 함께 봐야 합니다.
언제 infer를 붙일까
- 타입에 따라 결과를 달리한다: conditional type
- 함수 반환 타입을 추출한다:
infer R - 배열 원소 타입을 추출한다:
infer U - 유틸리티 타입 구현을 읽는다: conditional type 적극 활용
- 단순한 문제다: 일반 type alias 우선
주의할 점
conditional type을 과하게 중첩하면 타입 오류 메시지가 급격히 읽기 어려워집니다. 목적이 단순하면 더 직접적인 타입 별칭이 낫습니다.
// Pick, Omit, Record 같은 표준 유틸리티로 충분한지 먼저 확인참고 링크
1 sources