타입스크립트 커스텀 유틸리티 타입의 핵심 원리

by DD
13시간 전
조회수 0

내장 유틸리티 타입(Partial, Readonly, Pick, Omit 등)만으로 복잡한 프로젝트의 모든 타입 상황을 해결할 수 없으며, 이로 인해 커스텀 유틸리티 타입(Custom Utility Types) 필요성이 대두됨

Mapped Type으로 객체 프로퍼티를 순회하며 구조를 변환하고, Conditional Type으로 타입 조건 분기를 처리하는 메커니즘을 설명하며 동적 타입 생성(Dynamic Type Generation) 구현 방법을 제시함

문자열 타입 프로퍼티 추출 필터(StringTypeFilter)부분 옵셔널 변환(PartialOverwrite) 같은 실전 커스텀 타입을 직접 구현하여 두 도구의 시너지 확인함

추상화 수준(Abstract Level) 증가로 인한 IDE 성능 저하와 코드 가독성 저하를 언급하며, 실무 적용 시 재사용성(Reusability)과 유지보수성(Maintainability)의 균형 중요성을 강조함

두 도구의 이해를 통해 내장 유틸리티 타입의 동작 원리가 투명해지며, 개발자가 직접 타입을 조립하여 타입 안정성(Type Safety)을 한 단계 끌어올릴 수 있음을 강조함

Mapped Type의 순회 메커니즘과 구조 변환

Mapped Type은 객체 타입의 프로퍼티를 순회하면서 새로운 객체 타입을 만들어내는 문법적 구조(Syntactic Structure)이다. 자바스크립트의 for...in 루프와 유사한 개념으로, `keyof T`로 추출한 모든 프로퍼티 키를 유니온으로 묶고 `[K in keyof T]` 구문을 통해 각 키를 하나씩 꺼내 새로운 타입을 구성한다.

`keyof` 연산자는 타입 T의 모든 프로퍼티 키를 유니온 형태로 반환하며, 이는 순회 대상의 범위를 동적으로 결정하는 역할

`[K in keyof T]` 부분은 제네릭 컨텍스트에서 각 키를 순회하며, 이 시점에 프로퍼티 수정자(`?`, `readonly`)를 적용하여 변환된 결과 타입을 생성

타입스크립트 4.1 이후 도입된 `as` 키워드는 키 자체를 재매핑할 수 있어, 기존 키를 새로운 키로 치환하거나 `-?`, `-readonly`로 수식어를 제거하는 패턴 가능

이 메커니즘을 활용하면 Partial은 `[K in keyof T]?: T[K]`로, Readonly는 `[K in keyof T]: readonly T[K]`로 단 두 줄로 구현 가능하여 메타프로그래밍(Metaprogramming)적 표현력을 확인 가능

Conditional Type의 분기 로직과 분배적 동작

Conditional Type은 `T extends U ? X : Y` 형태의 삼항 연산자 구조로, T가 U에 할당 가능한지 검사한 뒤 결과에 따라 다른 타입을 반환하는 타입 수준 조건문(Type-Level Conditional)이다. 이 구조는 동적 타입 결정의 기반이 된다.

분배적 조건부 타입(Distributive Conditional Type): 검사 대상이 단일 타입 파라미터가 아닌 유니온일 경우, 타입스크립트는 유니온의 각 멤버에 조건을 분배하여 적용한다. `string | number extends number ? true : false`는 `string extends number ? true : false | number extends number ? true : false`로 분해

이 분배 동작을 활용하면 Exclude는 `T extends U ? never : T`로, Extract는 `T extends U ? T : never`로 구현 가능하며, never 타입의 자동 제거 메커니즘이 결과 타입을 정제

의도치 않은 분배를 방지하려면 `[T] extends [U] ? X : Y`처럼 튜플로 감싸는 패턴을 사용해야 하며, 이는 분기 로직을 엄격히 제어해야 할 때 필수적인 테크닉

infer 키워드를 활용한 타입 추론과 ReturnType 구현

infer는 Conditional Type 내부에서만 사용 가능한 상황적 타입 추론(Contextual Type Inference) 키워드로, 검사 대상의 특정 위치에 있는 타입을 추출하여 변수처럼 활용할 수 있게 한다. 이 메커니즘은 내장 유틸리티 타입의 복잡한 동작을 단순화하는 핵심 요소다.

`infer R`은 조건절 안에서 해당 위치의 타입을 R이라는 이름으로 바인딩하며, 이를 결과 타입에서 참조하여 반환 타입을 동적으로 결정

ReturnType은 `T extends (...args: any) => infer R ? R : never`로 구현되어 함수 시그니처에서 반환 타입만 추출하며, Parameters는 유사한 패턴으로 인자 타입을 튜플로 추출

Awaited도 같은 원리로 프로미스 타입에서 Promise<T>의 T를 추출하며, 중첩된 Promise를 재귀적으로 해제하는 동작을 지원

제약 사항: infer는 오직 Conditional Type의 extends 절 내에서만 사용 가능하며, 독립적인 타입 선언에서는 동작하지 않음

커스텀 유틸리티 타입의 실전 활용 패턴

{

"deep_dive": [

{

"content": "Mapped Type과 Conditional Type을 결합하면 내장 유틸리티 타입만으로는 표현하기 어려운 정교한 커스텀 타입을 구현할 수 있다. 두 도구가 협력하는 방식을 실제 코드에서 확인해 보자.\n• 문자열 타입 프로퍼티 추출(StringTypeFilter): Mapped Type으로 키를 순회하며 Conditional Type으로 값의 타입을 검사한다. 조건에 부합하지 않는 키는 never로 매핑되며, never로 매핑된 키는 결과 객체에서 자동으로 제거되는 필터링 효과(Filtering Effect)를 얻는다.\n• 부분 옵셔널 변환(PartialOverwrite): Omit으로 대상 키를 제거한 결과와 Pick 후 Partial을 적용한 결과를 교집합으로 합쳐 새로운 타입을 만든다. 폼 입력값이나 임시 저장 객체처럼 단계적으로 채워나가는 데이터를 다룰 때 유용한 점진적 빌드 패턴(Progressive Build Pattern)이다.\n• 이 두 예시는 Mapped Type이 구조 변환을, Conditional Type이 필터링을 담당하는 책임 분리(Separation of Concerns) 원칙을 따른다."

}

]

}

타입 추상화의 트레이드오프: 가독성, 성능, 유지보수

커스텀 유틸리티 타입은 표현력이 뛰어나지만, 실무 적용 시 고려해야 할 중요한 트레이드오프가 존재한다. 추상화 수준이 높아질수록 코드 가독성과 타입 추론 성능 사이의 균형을 신중히 판단해야 한다.

가독성 이슈: 복잡한 Conditional Type의 중첩은 코드를 읽는 다른 개발자에게 높은 인지 부담을 주며, 팀 단위 프로젝트에서는 일회성 타입은 인라인으로 작성하고 자주 쓰이는 패턴만 유틸리티로 분리하는 것이 권장

IDE 성능 영향: Conditional Type을 깊게 중첩하면 타입 추론 비용이 증가하여 IDE 응답 속도가 떨어질 수 있으며, 큰 프로젝트에서 특히 체감됨

유지보수성: 복잡한 타입은 적절히 분리하고 이름을 붙여 재사용 가능하게 만들어야 하며, 주석과 문서화를 통해 의도와 사용법을 명확히 전달하는 것이 필수

결국 표현력(Expressiveness)과 실용성(Practicality) 사이의 균형 점이 존재하며, 이를 잘 파악하는 것이 고급 타입 작성의 핵심 역량

나만의 유틸리티 타입 만들기: Mapped Type, Conditional Type