우당탕탕

TypeScript 제네릭 타입 추론, 2026년 바뀐 점까지 제가 겪은 경험 담았어요 본문

언어/JavaScript

TypeScript 제네릭 타입 추론, 2026년 바뀐 점까지 제가 겪은 경험 담았어요

모찌모찝 2026. 8. 4. 14:13

TypeScript 제네릭 타입 추론을 구현하다가 생각보다 삽질을 많이 했어요. 특히 2026년 들어 바뀐 타입스크립트 컴파일러 동작 때문에 예전 코드가 안 먹히는 경우가 점점 늘더라고요. 저도 이 부분에서 한참 헤맸는데, 이번에 정리하면서 헷갈렸던 부분부터 최신 변경점까지 꼼꼼히 정리해봤습니다.

이 글에서는 TypeScript 제네릭 타입 추론의 기본원리부터 시작해서, 2026년 기준 바뀐 사항, 그리고 제가 직접 구현하며 겪은 구체적인 문제와 해결책까지 다루려고 해요. 실제 코드를 보면서 이해하면 훨씬 수월할 거예요.

개발 환경 / 버전 정보

이번 정리를 하면서 사용한 TypeScript 버전은 TS 5.2.2이고, Node.js 20, React 18 환경에서 작업했어요. 예전에는 4.x 버전을 쓰다가 최근 5.x로 올라가면서 타입 추론 쪽에 꽤 큰 변화가 있더라고요. 최신 TS 버전이 어느 정도인지 꼭 확인하세요.

TypeScript 제네릭 타입 추론 헷갈렸던 부분 정리 관련 이미지

TypeScript 제네릭 타입 추론 헷갈렸던 부분 정리 관련 정보

기본적인 제네릭 타입 추론 이렇게 됩니다

사실 TypeScript의 제네릭 타입 추론은 함수 호출 시 타입 파라미터를 명시하지 않아도 컴파일러가 호출 인자에서 타입을 유추하는 방식이에요. 기본적인 형태부터 짚고 넘어가면 쉬워서 먼저 간단한 예시를 볼게요.

function identity<T>(arg: T): T {
  return arg;
}

const result = identity(42);  // T는 여기서 number로 추론됨

여기서 호출 시점의 인자 42가 number라서 컴파일러가 T = number로 자동 추론해요. 이게 제네릭 타입 추론의 기본 원리입니다.

2026년에 달라진 점, '널리 알려지지 않은' 타입 추론 변경사항

그런데 여기서 막히는 분들 많을 거예요. 저도 그랬고요. 작년까지만 해도 잘 되던 코드가 갑자기 '타입이 추론되지 않습니다'라는 에러를 내는 경우가 있어서 한참 헤맸는데요, 2026년에 들어서 TypeScript 5.1 이후 부터 추론된 제네릭 타입의 좁은 타입에서 넓은 타입으로 자동 변환되는 로직이 강화됐어요.

예를 들어 이전에는 stringstring | undefined가 혼용되어도 성큼성큼 넘어갔는데, 이제는 이 부분에서 컴파일러가 더 까다롭게 동작하면서, 명시적으로 타입을 맞춰줘야 하는 경우가 종종 발생합니다.

예시: 함수 매개변수 타입이 달라져서 에러가 난 경우

function wrapValue<T>(value: T) {
  return { value };
}

const myVal = wrapValue(null);   // 이전엔 T가 null로 추론됐음

// 2026년형 TS에서는 T가 unknown으로 추론되거나 에러가 뜰 수 있음

이럴 때는 타입 매개변수를 명시적으로 적어주거나, 아니면 as 단언으로 명확히 해줘야 합니다.

TypeScript 제네릭 타입 추론 헷갈렸던 부분 정리 직접 정리한 자료

TypeScript 제네릭 타입 추론 헷갈렸던 부분 정리 관련 정보

실제 구현하면서 겪은 문제와 코드

저도 제네릭 타입 추론을 활용해서 공통 유틸 함수를 만들다 보니까 이렇게 복잡한 상황이 생기더라고요. 특히 파라미터가 함수나 옵셔널 값, 그리고 null/undefined가 섞인 타입인 경우가 헷갈렸어요. 이 부분은 실제 코드로 보여주는 게 이해에 도움이 될 거예요.

function applyCallback<T, U>(input: T, callback: (arg: T) => U): U {
  return callback(input);
}

// 제대로 동작하는 예시
const output1 = applyCallback(123, (num) => num.toString());

// undefined를 넣을 때 타입 에러 발생 가능 (TS 5.1+)
const output2 = applyCallback(undefined, (val) => val ?? 'default'); // 에러가 나기도 함

// 해결책: T 명시 및 옵셔널 타입 처리
const output3 = applyCallback<number | undefined, string>(undefined, (val) => val ? val.toString() : 'default');

이 예시에서 output2가 애매한 값을 넣었을 때 타입 추론이 꼬여서 에러가 났던 거예요. 이럴 때는 타입 파라미터를 명시적으로 넣어주거나, 호출자 쪽에서 타입을 확실히 정해주는 게 좋더라고요.

여기서 삽질했던 부분들

정말 한참 막혔던 부분은 함수 오버로드와 제네릭 추론이 꼬이는 문제였어요. 특히 복잡한 유틸 함수 만들 때, 오버로드 시그니처와 실제 구현 시그니처가 다르면 컴파일러가 헷갈리는 경우가 많더라고요.

// 문제 코드: 오버로드와 구현이 달라서 에러 발생
function fetchData(url: string): Promise<string>;
function fetchData(url: string, json: true): Promise<any>;
function fetchData(url: string, json?: boolean) {
  if (json) {
    return fetch(url).then(res => res.json());
  }
  return fetch(url).then(res => res.text());
}

// 호출 시 json이 true일 때 타입 추론이 이상하게 작동할 수 있음

이 문제는 함수 오버로드 정의와 구현 시그니처를 최대한 일치시키거나, 제네릭 타입을 오버로드 시그니처에 잘 녹여 넣어야 해결됐어요. 실제 코드 수정 전과 후를 비교해 볼게요.

// 개선된 코드
function fetchData<T = string | any>(url: string, json?: boolean): Promise<T> {
  if (json) {
    return fetch(url).then(res => res.json());
  }
  return fetch(url).then(res => res.text());
}

// 호출 시
const result1 = fetchData('some-url');
const result2 = fetchData<object>('some-url', true);

이렇게 명시적으로 타입 파라미터를 넣거나, 기본 타입을 정해주니까 훨씬 타입 오류가 줄더라고요.

TypeScript 제네릭 타입 추론 헷갈렸던 부분 정리 참고 사진

TypeScript 제네릭 타입 추론 헷갈렸던 부분 정리 관련 정보

더 알면 좋은 팁: 인수 위치와 타입 추론 관계

그런데 여기서 한 가지 중요한 팁 드리자면, 제네릭 타입 추론은 함수의 매개변수 위치 및 개수에 따라 결과가 달라질 수 있어요. 예를 들어, 여러 개의 제네릭 매개변수를 사용할 때 첫 번째 인수에서만 타입을 뽑고, 나머지는 뺄 수도 있고, 순서가 바뀌면 추론이 꼬이기도 해서요. 실제로 제가 경험한 케이스를 보여드릴게요.

function combine<T, U>(a: T, b: U): [T, U] {
  return [a, b];
}

const combo1 = combine(123, 'abc'); // [number, string] - 잘 추론됨
const combo2 = combine('abc', null); // [string, null] - 문제 없음

// 그런데 이렇게 쓸 때
function combineSwap<T, U>(b: U, a: T): [T, U] {
  return [a, b];
}

const combo3 = combineSwap('abc', 123); // 추론이 헷갈릴 수 있음

매개변수 순서에 따라 컴파일러가 각각의 제네릭 타입을 유추하는 기준이 달라져서, 의도치 않은 타입 추론 오류가 생길 수 있다는 점 기억하세요.

자주 물어보는 질문들

Q. 제네릭 타입 추론에서 왜 null이나 undefined가 문제인가요?

A. null이나 undefined는 넓은 의미의 허용 가능한 값이라서, 예전에는 그런 값이 들어와도 타입 추론이 유연하게 넘어갔는데, 5.x 이후부터는 좁은 타입과 넓은 타입 사이에서 엄격한 구분이 생겨서 자동 추론이 어려워진 거예요. 그래서 명시적 타입 선언을 해주거나, 기본 타입을 지정해주는 게 안전합니다.

Q. 타입 파라미터를 왜 꼭 명시적으로 적어야 할 때가 있나요?

A. 타입스크립트가 모든 상황에서 완벽히 타입을 추론할 수 없기 때문이에요. 특히 앞서 언급한 함수 오버로드, 옵셔널 인자, null/undefined가 섞인 복잡한 타입 상황에서는 컴파일러가 헷갈려서 에러가 나거든요. 이럴 때는 직접 <T> 형식 매개변수를 명시해주면 명확하게 타입이 결정됩니다.

Q. 2026년 이후 TypeScript 버전별로 버그가 더 생기지는 않을까요?

A. 매 릴리즈마다 일부 변경점이나 개선사항, 심지어 리그레션이 있을 수 있어요. 그런데 5.x 버전에서 특히 타입 추론 로직이 크게 바뀌면서 이전에 별 문제 없던 코드가 갑자기 에러가 나는 경우가 늘었어요. 그래서 프로젝트에서 사용하는 TS 버전을 엄격하게 관리하거나, 변경점 노트를 꼭 살펴보면서 마이그레이션 하는 걸 권장합니다.

결국 제네릭 타입 추론은 편리하지만, 복잡해질수록 직접 타입을 명시하거나, 함수 시그니처를 명확히 해줘야 안정적으로 쓸 수 있다는 걸 이번에 뼈저리게 느꼈어요. 특히 2026년 기준으로 타입스크립트가 더 엄격해져서, 예전 코드 쓸 때보다 주의해야 할 점들이 많아졌습니다. TypeScript 버전 업데이트할 때마다 기존 제네릭 코드를 꼼꼼히 점검하는 작업이 꼭 필요하다는 점, 저도 이 과정에서 배웠네요.

Comments