FEInterview Prep

typescript · high priority

TypeScript Generics — 타입을 *함수처럼* 다루는 법

타입 파라미터 · 제약 · 조건부 타입 · `infer` · `const` 타입 파라미터 · `NoInfer`

intermediate 난이도5시간토스카카오네이버당근배민라인쿠팡
시작 전
이해도
매우 낮음

학습 개요

탄생 배경

쉬운 설명

복잡한 개념을 실생활 비유로 설명합니다.

*레시피의 재료 자리*

함수가 "x 를 받아 x+1 을 반환" 한다면, Generic 함수는 "*어떤 재료* 든 받아 *그 재료의 잘 다진 형태* 를 반환하는 레시피" 입니다. 레시피의 "당근" 자리에 *닭고기* 를 넣어도 동작이 그대로이고, 결과의 형태(잘게 다진 닭고기) 까지 *재료에 비례* 해서 따라옵니다. `extends` 제약은 "이 자리에는 *채소만* 들어갈 수 있어요" 같은 안내문이고, `infer` 는 "지금 들어온 재료의 *부피* 만 변수로 빼서 따로 쓸게요" 같은 일이며, `NoInfer` 는 "*이번엔* 재료 결정에 이 자리는 끼지 마세요" 라는 약속입니다.

핵심 개념

`any` 로 푼 *불안전한* 풀이 vs Generic 풀이

❌ `any` — 타입 안전성 *증발*
  • 입력의 타입 정보를 *반환에서 잃어버림*
  • 호출자가 *반환을 다시 단언* 해야 함
  • 오타·잘못된 프로퍼티 접근이 *컴파일러를 통과*
1function first(arr: any[]): any {
2 return arr[0];
3}
4
5const n = first([1, 2, 3]);
6n.toUpperCase(); // 컴파일 OK, 런타임 폭발
✅ Generic — *관계* 가 보존됨
  • T 한 변수로 *입력→출력 관계* 표현
  • 호출 시점 타입에 따라 반환 타입 *자동 추론*
  • 오타·잘못된 메서드 호출이 *컴파일 타임에 차단*
1function first<T>(arr: T[]): T | undefined {
2 return arr[0];
3}
4
5const n = first([1, 2, 3]); // n: number | undefined
6n?.toUpperCase(); // ❌ Property 'toUpperCase' does not exist on type 'number'

`any`

*모든 타입* 으로 가정하고 *모든 검사를 끈다*. 외부 데이터의 *임시 봉합* 외에는 거의 사용하지 말 것.

`unknown`

"무엇인지 모름" 의 *안전한* 표현. *어떤 연산도 못 하게* 강제하므로 *type narrowing* (typeof, 타입 가드) 후에만 사용 가능.

Generic `T`

*호출 시점에 결정되는* 타입 변수. 입력 타입과 출력 타입의 *동일성/관계* 를 보존한다. any 가 아니라 *실제 타입* 으로 좁혀진다.

실전 가이드 — *경계엔 `unknown`, 내부엔 `T`*

API 응답·JSON.parse 같은 *외부 경계* 에서는 unknown 으로 받고 Zod 같은 검증기로 좁힌다. 내부 유틸 함수는 *Generic* 으로 입출력 관계를 표현한다. any 는 *마이그레이션 임시* 외에는 정당화하기 어렵다.

실무 적용

어떤 상황에서 사용하는가

팀의 *API 클라이언트 래퍼* 와 *폼 헬퍼* 를 동시에 만들어야 한다. 엔드포인트마다 응답 타입이 다르고, 폼은 필드 enum 을 *리터럴로 보존* 해야 하며, 외부 응답은 모두 검증을 거친다.

어떻게 적용하는가

API 응답 입구는 *`unknown` + Zod* 로 받고 *통과한 값에서만* `Generic T` 가 등장하게 한다. `apiCall<T extends ZodSchema>(schema: T): z.infer<T>` 식으로 *스키마 한 번 정의로 런타임·타입 양쪽* 을 잡는다. 폼 헬퍼는 `createForm<const Fields extends readonly Field[]>` 로 *리터럴 보존*, 기본값 인자는 `NoInfer<...>` 로 추론 출처를 *필드 정의 한 곳* 으로 잠근다. 매핑 타입으로 `Errors<Fields>`, `Values<Fields>` 같은 *파생 타입* 을 자동 생성한다.

흔한 실수와 안티패턴

  • Generic 이름을 늘 `T, U` 로 두면 가독성이 무너진다 — `TData`, `TError`, `TVariables` 처럼 *역할 기반 이름*.
  • 제약을 *너무 좁게* (`<T extends User>`) 두면 재사용성이 사라진다 — 본문이 실제로 요구하는 최소 형태만.
  • `as` 단언을 함수 본문 안에서 남발하면 *Generic 의 의미가 사라짐* — 시그니처를 다시 본다.
  • 조건부 타입의 *유니온 분배* 를 잊고 결과가 `"yes" | "no"` 로 갈라져 디버깅이 어려워진다 — `[T] extends [U]` 트릭.
  • `any[]` 를 받아 Generic 으로 변환하면 *경계의 안전성* 이 깨진다 — 외부는 `unknown`, 내부는 `T`.
  • `NoInfer` 를 모르면 다중 인자 함수에서 *원치 않는 위치* 에서 추론이 일어나 디폴트 값이 의도와 다른 타입을 강요한다.

흔한 오해

오해

*"Generic 은 라이브러리 작성자만 쓰는 고급 기능"*

교정

`useState<User | null>(null)`, `useQuery<User>`, `Array<number>` 모두 평범한 Generic 호출. 매일 *읽기* 는 거의 모든 FE 가 한다. *쓰기* 도 작은 유틸에서 시작된다.

왜 중요

도구 라이브러리들이 *대부분 Generic 시그니처* 를 가지므로 안 쓰면 시그니처를 못 읽고 IDE 도움도 못 받는다.

오해

*"`any` 와 `unknown` 은 거의 같다"*

교정

`any` 는 *검사 비활성*, `unknown` 은 *검사가 켜진 채로 좁히기를 강제*. `unknown` 값을 그냥 호출하거나 프로퍼티에 접근하면 *컴파일 에러*.

왜 중요

`unknown` 의 존재 이유는 *경계의 안전한 입구* 를 만드는 것. `any` 의 위험성을 그대로 두지 않으려는 설계.

오해

*"`extends` 는 상속 (inheritance) 이다"*

교정

Generic 의 `extends` 는 *상한 (upper bound)*, 즉 *부분 타입 관계의 제약*. 클래스 상속과 키워드만 같을 뿐 의미가 다르다. `T extends U` 는 "T 가 U 에 *할당 가능* 해야 함".

왜 중요

구조적 타입 시스템에서 *모양만 같으면* 할당 가능. *명목적 상속* 이 아니라 *형태 기반 호환*.

오해

*"Generic 을 많이 쓸수록 타입 안전성이 좋아진다"*

교정

*불필요한 Generic* 은 시그니처만 어렵게 만든다. 본문이 `T` 를 *한 자리에서만* 사용한다면 그 함수에 Generic 은 보통 필요 없다 — 그 자리의 *구체 타입* 으로 충분.

왜 중요

Generic 의 가치는 *입출력 사이의 관계 보존* 에 있음. 관계가 없으면 도입할 이유도 없다.

면접 질문

중급토스카카오네이버라인

답변 방향 힌트

"검사 끔 vs 안전한 미지 vs 관계 보존" 으로 시작.

반드시 언급할 키워드

  • `any` — 타입 검사 비활성, *모든 연산 통과* — 임시 봉합용
  • `unknown` — 미지의 타입을 *안전하게* 표현, 좁히기 전 모든 연산 차단
  • Generic `T` — *호출 시점 결정*, 입력→출력 관계 보존
  • 경계는 `unknown`, 내부 유틸은 `T`
  • `unknown` → Zod / 타입 가드로 좁히고 그 결과를 `T` 로 흘려보내기

예상 꼬리 질문

  • Zod 의 `z.infer<typeof schema>` 가 *어떻게 타입을 추출* 하나요?
  • `unknown` 값을 좁히는 *세 가지 흔한 방법* 을 들어 주세요.

자기 점검

Generic 함수와 `any` 를 받는 함수의 *결정적인* 차이를 한 문장으로.

기대 키워드

입력→출력관계 보존컴파일 타임

자주 하는 오해

*"둘 다 어떤 타입이든 받는다"* — 받기는 같지만 *반환 타입의 정확도* 가 다르다.

`Partial<T>` 를 매핑 타입으로 *직접 작성* 해 보라.

기대 키워드

`[K in keyof T]``?:``T[K]`

자주 하는 오해

*"인터페이스 상속으로 흉내낼 수 있다"* — 매핑 타입의 *구조적 변형* 은 상속으로 표현 불가.

`T extends U ? A : B` 에 *유니온 T* 가 들어가면 어떤 일이 벌어지는가?

기대 키워드

분배distributive각 멤버에 적용

자주 하는 오해

*"T 전체를 한 덩어리로 비교한다"* — 분배 차단을 원하면 `[T] extends [U]`.

`NoInfer<T>` 는 *어떤 시나리오* 에서 등장하는가?

기대 키워드

같은 T 여러 자리추론 출처 잠금TS 5.4

자주 하는 오해

*"`NoInfer` 는 추론 자체를 끄는 옵션"* — 다른 자리의 추론은 그대로, *해당 자리만* 추론에서 제외.

학습 자료