제네릭(Generics)은 타입을 매개변수처럼 받아, 여러 타입에 같은 로직을 재사용하면서도 타입 정보를 잃지 않게 하는 기능이다.
function first<T>(arr: T[]): T { return arr[0] } // <T>: 타입 변수 선언
first<string>(['a']) // <string>: T에 string 대입
first([1, 2]) // 보통은 추론으로 T = number- 타입 매개변수는 반환 타입이 아니다.
<string>은 T에 값을 채울 뿐이고, 반환 타입은 시그니처가 정한다.reduce<Record<string, User[]>>(…)가 결과 타입처럼 보이는 건reduce가 우연히U를 반환하기 때문이다(reduce<U>(callbackfn, initialValue: U): U모양) - any와의 차이:
any[]를 받으면 결과도any라 오류가 숨는다. 제네릭은 호출 시점에 타입이 정해져 검사가 유지된다 - 제약(Generic Constraints,
extends):<T extends { length: number }>처럼 받을 수 있는 범위를 좁힌다.<T, K extends keyof T>(obj: T, key: K): T[K]는 실제 있는 키만 받는다(keyof) - 반환 타입 T는 호출자가 넘긴 바로 그 타입이다.
<T extends { length: number }>(obj: T, min: number): T에서{ length: min }을 반환하면 에러다. 이 객체는 제약은 만족하지만, 시그니처는 '제약에 맞는 아무 객체'가 아니라 '넘겨받은 것과 같은 타입'을 돌려준다고 약속했기 때문이다. 배열을 넘긴 호출자는 결과에slice가 있다고 믿는다 - 인스턴스마다 타입을 고정하고 싶으면 제네릭 클래스(
DataStorage<string>), 섞여도 되면 유니언을 쓴다 - React의
useState<number>,useRef<HTMLInputElement>(null)도 제네릭이다
좋은 제네릭 쓰기 (Handbook 지침)
- 타입 매개변수는 제약보다 실제 쓰이는 곳에 둔다:
<T>(arr: T[])가<T extends any[]>(arr: T)보다 추론이 정확하다 - 가능한 한 적게 쓴다
- 한 곳에만 나오는 타입 매개변수는 필요 없을 가능성이 높다
타입을 계산하는 쪽으로 나아가면 조건부 타입과 infer·유틸리티 타입가 된다. 과한 일반화는 YAGNI를 떠올린다.
출처: TypeScript Handbook - Generic Functions · Working with Constrained Values · Guidelines for Writing Good Generic Functions