TypeScript의 새로운 const type parameter 구문은 호출자가 모든 곳에 as const를 뿌려야 하는 번거로움 없이 함수가 리터럴 타입을 그대로 유지할 수 있게 해주며, 타입 확장(type-widening) 버그의 가장 흔한 원인을 제거합니다.

제네릭 코드를 괴롭히는 타입 확장 문제

제네릭 함수가 객체 리터럴을 받으면, 컴파일러는 모든 리터럴 속성을 더 넓은 범위의 기본(primitive) 타입으로 확장합니다.

function call<T>(arg: T) {}
call({ method: "GET" })   // T is inferred as { method: string }

리터럴 "GET"string으로 축소됩니다. 판별된 유니온(discriminated unions)이나 템플릿 리터럴 추출과 같이 정확한 값에 의존하는 후속 코드는 타입이 더 이상 정밀한 리터럴을 포함하지 않기 때문에 깨지게 됩니다. 개발자들은 오랫동안 호출부에서 { method: "GET" } as const라고 작성하여 컴파일러에게 리터럴을 유지하도록 지시하는 방식으로 이를 우회해 왔지만, 이러한 해결책은 함수의 정의가 아닌 호출자의 손에 달려 있습니다.

Const type parameters: 시그니처 수준의 해결책

타입 매개변수에 추가된 새로운 const 수식어는 컴파일러에게 해당 제네릭 인자에 대해 가능한 가장 좁은(narrowest) 타입을 추론하도록 지시합니다. 함수를 function foo<const T>(arg: T)와 같이 선언하면, T는 호출자가 as const를 작성한 것처럼 자동으로 동작합니다.

  • 문자열, 숫자, 불리언 리터럴은 정확한 값을 유지합니다 (string 대신 "GET").
  • 배열은 각 요소가 정밀하게 타입 지정된 readonly 튜플이 됩니다.
  • 객체는 모든 중첩 수준에서 리터럴 타입을 보존하는 깊은(deeply) readonly 구조로 변합니다.

제약 조건이 함수 시그니처에 존재하기 때문에 모든 호출자가 자동으로 혜택을 받습니다. 이제 타입 캐스팅을 잊어버린다고 해서 타입 안전성이 깨지는 일은 없습니다.

기존의 as const 해킹보다 뛰어난 이유

as const호출자 측(caller-side) 해결책입니다. 제네릭 함수를 사용하는 모든 소비자가 단언(assertion)을 추가해야 함을 기억해야 합니다. 단 한 번의 호출이라도 놓치면 타입 안전성은 사라집니다. 반면 const type parameter는 그 책임을 API 설계 자체로 옮깁니다. 함수가 "전달되는 무엇이든 가장 좁은 형태가 필요합니다"라고 선언하면, 컴파일러가 이를 강제합니다.

이러한 변화는 제네릭 빌더, 설정 팩토리 또는 필드의 리터럴 값이 타입 로직을 결정하는 API를 제공하는 라이브러리 및 유틸리티에 가장 중요합니다. 라이브러리 작성자는 후속 코드를 일일이 감시하지 않고도 정확한 추론을 보장할 수 있습니다.

이점을 얻을 수 있는 실제 시나리오

  • 설정 빌더(Configuration builders) – 환경 이름("dev" | "prod")이 리터럴로 유지되어, 추가적인 캐스팅 없이도 판별된 유니온 체크가 가능합니다.
  • API 라우트 정의 – 경로 문자열이 정확하게 유지되어, 템플릿 리터럴 타입이 파라미터를 추출할 수 있습니다 ("/users/:id"\/users/${string}``).
  • 상태 머신 헬퍼(State-machine helpers) – 상태 식별자가 메서드 체이닝을 통해서도 고정된 리터럴로 유지되어, 실수로 인한 상태 불일치를 방지합니다.

각 사례에서 const 매개변수는 반복되는 as const 보일러플레이트를 제거하고 미묘한 버그가 발생할 가능성을 줄여줍니다.

satisfies 연산자와의 조합

satisfies 연산자는 원래의 리터럴 정보를 보존하면서 값이 구조적 타입에 부합하는지 검증합니다. 이 둘을 함께 사용하면 두 방식의 장점을 모두 누릴 수 있습니다. const 매개변수는 좁은 추론을 제공하고, satisfies는 값이 요구되는 형태를 충족하는지 확인합니다.

function makeConfig<const C>(cfg: C) {
  // cfg is inferred with exact literals
}
const cfg = {
  env: "staging",
  ports: [8080, 8443],
} satisfies { env: string; ports: number[] };
makeConfig(cfg); // works, literals stay intact

as const를 계속 사용해야 할 때

const 매개변수는 함수의 시그니처를 직접 제어할 수 있을 때 빛을 발합니다. 만약 해당 수식어가 없는 서드파티 함수를 다루거나, 로컬 변수에 대해 일회성으로 리터럴 보존이 필요한 경우에는 as const가 여전히 적절한 도구입니다. 호출되는 API를 변경하지 않고 값을 고정하는 가장 일반적인 방법으로 계속 사용될 것입니다.

앞으로 주목할 점

이 기능은 아직 최신 기능이므로 도구 및 커뮤니티 패턴이 진화하고 있습니다. 자동 완성 및 빠른 수정(quick-fix) 제안에서 새로운 구문을 보여주는 IDE 지원 업데이트를 기대해 보세요. 라이브러리 유지 관리자들을 주목하십시오. 많은 라이브러리가 공개 제네릭을 const 매개변수로 마이그레이션하기 시작할 것이며, 이는 이전에 명시적인 as const 캐스팅에 의존했던 코드에 breaking change(파괴적 변경 사항)를 일으킬 수 있습니다.

핵심 요약: 리터럴 보존 기능을 함수의 타입 매개변수에 직접 내장함으로써, TypeScript의 const type parameter는 타입 확장 오류의 흔한 원인을 제거하고 안전성의 책임을 호출자에서 API 설계자로 되돌려 놓습니다. 직접 관리하는 모든 제네릭 진입점에 이 기능을 사용하세요. as const는 로컬 값이나 외부 API를 위해 남겨두십시오.