제네릭과 타입 레벨 프로그래밍
- 6.1 제네릭은 타입 간의 함수
- 6.2 불필요한 타입 매개변수 피하기
- 6.3 조건부 타입으로 유니온 분배하기
- 6.4 템플릿 리터럴 타입으로 문자열 조합하기
- 6.5 타입 수준 코드도 테스트하기
- 6.6 꼬리 재귀와 타입 수준 재귀
제네릭을 "그냥 꺾쇠 괄호에 T 넣는 거"로만 알고 있었다면, 이 장을 읽고 나면 생각이 완전히 바뀔 거야.
제네릭은 타입 세계의 함수거든. 일반 함수가 값을 받아서 값을 반환하듯, 제네릭 타입은 타입을 받아서 타입을 반환해. Array<string>은 Array라는 "타입 함수"에 string이라는 "타입 인수"를 넘긴 거야. 이 관점이 잡히면 TS의 타입 시스템이 하나의 함수형 프로그래밍 언어라는 게 보여. 타입 매개변수는 함수 매개변수, extends는 조건문, 매핑된 타입은 map, 조건부 타입은 삼항 연산자인 거지. 제네릭 제약조건(K extends keyof T)도 값 세계의 함수 매개변수에 유효성 검사를 거는 것과 같은 맥락이야.
근데 제네릭을 배우면 과하게 쓰고 싶어지거든. 타입 매개변수가 한 곳에서만 쓰이면 정말 필요한지 의심해야 해. 타입 매개변수가 정당화되려면 두 가지 이상의 타입을 연결해야 해. 입력과 출력을 연결하거나, 여러 매개변수의 타입을 연결하는 식으로. 한 곳에서만 쓰이면 그냥 구체적인 타입이나 unknown으로 대체할 수 있어.
**조건부 타입(Conditional Types)**은 타입 레벨의 삼항 연산자야. T extends string ? 'yes' : 'no' 이런 식인데, 유니온 타입을 넣으면 각 멤버에 대해 개별적으로 적용되는 분배(distribution) 규칙이 들어가면서 강력해져. never가 유니온에서 사라지는 성질을 이용하면 특정 타입만 추출하거나 제거할 수 있는데, TS 표준 라이브러리의 Extract와 Exclude가 정확히 이 패턴이야. infer 키워드를 쓰면 패턴 매칭도 가능해서, ReturnType<T>처럼 함수의 반환 타입을 추출하는 것도 할 수 있지.
템플릿 리터럴 타입은 문자열 리터럴 타입을 조합할 수 있게 해줘. 유니온과 결합하면 ${Color}-${Size} 같은 조합 폭발을 자동으로 만들어내거든. CSS 속성 이름, 이벤트 이름, API 경로 같은 곳에서 실전적으로 쓰여.
복잡한 타입을 만들었으면 테스트도 해야 해. expect-type 같은 라이브러리를 쓰면 타입이 의도대로 동작하는지 검증할 수 있어. 특히 라이브러리를 만들 때는 공개 API의 타입 테스트가 필수야. 주의할 점은 any가 거의 모든 타입 테스트를 통과해버리니까, any가 아닌지도 체크해야 한다는 거지.
타입 레벨에서 재귀도 할 수 있는데, TS의 재귀 깊이 제한이 있어. TS 4.5부터 꼬리 재귀 최적화가 도입돼서, 누적 매개변수(accumulator)를 써서 재귀를 꼬리 위치로 바꾸면 더 깊이 들어갈 수 있어. 대부분의 실무에서 직접 쓸 일은 많지 않지만, 라이브러리 타입 설계할 때는 알아야 해.
정리
6장 읽고 기억할 거 세 가지:
- 제네릭은 타입 세계의 함수다. 이 관점이 잡히면 TS의 타입 시스템이 하나의 프로그래밍 언어로 보인다.
- 불필요한 타입 매개변수는 피하라. 한 곳에서만 쓰이는 타입 매개변수는 거의 항상 불필요하다.
- 조건부 타입 + 분배 + infer가 타입 레벨 프로그래밍의 핵심 도구다.
Extract,Exclude,ReturnType같은 표준 유틸리티 타입이 전부 이 패턴으로 만들어져 있다.