템플릿 리터럴 타입 — 문자열 타입 조합과 추론

TypeScript 템플릿 리터럴 타입의 문법, 유니언 배포, 내장 문자열 조작 타입(Uppercase·Capitalize 등), infer와의 결합 패턴을 완전히 정리합니다.

· 5 min read · PALDYN Team

지난 글에서 매핑된 타입으로 기존 타입의 속성을 일괄 변환하는 방법을 배웠다. 이번에는 **템플릿 리터럴 타입(Template Literal Types)**을 살펴본다. TypeScript 4.1에서 도입된 이 기능은 JavaScript의 백틱 문자열처럼 타입 수준에서 문자열을 조합하고, 패턴 매칭으로 부분 문자열을 추출할 수 있게 해준다.

기본 문법

템플릿 리터럴 타입은 JavaScript 템플릿 리터럴과 동일한 백틱 문법을 사용한다.

type Greeting = `Hello, ${string}`;
// 모든 "Hello, ..."로 시작하는 문자열

type Id = `user_${number}`;
// "user_0", "user_1", "user_42" 등

type EventName = `on${"Click" | "Focus" | "Blur"}`;
// "onClick" | "onFocus" | "onBlur"

삽입 위치에 유니언 타입을 넣으면 모든 조합의 유니언이 자동으로 만들어진다.

유니언 배포

두 유니언을 조합하면 곱집합이 만들어진다.

type Color = "red" | "blue";
type Size = "sm" | "lg";

type ClassName = `${Color}-${Size}`;
// "red-sm" | "red-lg" | "blue-sm" | "blue-lg"

템플릿 리터럴 타입 문법

내장 문자열 조작 타입

TypeScript는 타입 수준의 문자열 변환을 위한 네 가지 내장 타입을 제공한다.

type A = Uppercase<"hello">;      // "HELLO"
type B = Lowercase<"HELLO">;      // "hello"
type C = Capitalize<"hello">;     // "Hello"
type D = Uncapitalize<"Hello">;   // "hello"

// 매핑된 타입과 결합: camelCase 키 생성
type CamelKeys<T> = {
  [K in keyof T as Uncapitalize<string & K>]: T[K];
};

이 타입들은 컴파일러 내장(intrinsic) 타입이라 구현 코드가 없지만, 사용 방식은 일반 타입 별칭과 동일하다.

이벤트 핸들러 패턴

매핑된 타입과 결합하면 API를 자동으로 파생할 수 있다.

type ButtonEvents = {
  click: MouseEvent;
  focus: FocusEvent;
  keydown: KeyboardEvent;
};

type Handlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (e: T[K]) => void;
};

type ButtonHandlers = Handlers<ButtonEvents>;
// {
//   onClick: (e: MouseEvent) => void;
//   onFocus: (e: FocusEvent) => void;
//   onKeydown: (e: KeyboardEvent) => void;
// }

실전 패턴

infer로 문자열 분해

조건부 타입의 infer와 결합하면 문자열 패턴에서 부분을 추출할 수 있다.

// 접두사 제거
type StripPrefix<S, P extends string> =
  S extends `${P}${infer Rest}` ? Rest : S;

type T1 = StripPrefix<"onClick", "on">;  // "Click"

// 경로에서 파일명 추출
type Filename<S extends string> =
  S extends `${string}/${infer File}` ? File : S;

type T2 = Filename<"src/components/Button.tsx">;  // "Button.tsx"

CSS 속성 타입 안전성

CSS in JS 라이브러리나 유틸리티 CSS 프레임워크에서 유용하다.

type Side = "top" | "right" | "bottom" | "left";
type Spacing = "margin" | "padding";

type SpacingProp = `${Spacing}-${Side}`;
// "margin-top" | "margin-right" | ... | "padding-left"

// px 단위 검증
type PxValue = `${number}px`;
function setWidth(value: PxValue) { /* ... */ }

setWidth("100px");  // OK
setWidth("100");    // 오류: PxValue 아님

타입 안전 EventEmitter

type Events = {
  connect: { url: string };
  disconnect: { code: number };
  message: { data: string };
};

interface TypedEmitter<T> {
  on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void;
  emit<K extends keyof T>(event: K, payload: T[K]): void;
}

// 컴파일러가 이벤트 이름과 페이로드 타입을 모두 검증
declare const emitter: TypedEmitter<Events>;
emitter.on("connect", ({ url }) => console.log(url));  // OK
emitter.emit("message", { data: "hello" });            // OK

재귀와 성능 고려

// camelCase를 kebab-case로 변환 (재귀)
type CamelToKebab<S extends string> =
  S extends `${infer Head}${infer Tail}`
    ? Head extends Uppercase<Head>
      ? `-${Lowercase<Head>}${CamelToKebab<Tail>}`
      : `${Head}${CamelToKebab<Tail>}`
    : S;

재귀 템플릿 리터럴 타입은 강력하지만 컴파일 타임 비용이 높다. 프로덕션 코드에서 긴 문자열이나 깊은 재귀를 사용할 때는 TypeScript 컴파일러의 재귀 깊이 제한(~100단계)과 성능 영향을 고려해야 한다. 다음 글에서는 infer 키워드를 더 깊이 살펴본다.


지난 글: 매핑된 타입 — 기존 타입을 순회해 새 타입 만들기

다음 글: infer 키워드 — 타입 추론의 고급 활용


읽어주셔서 감사합니다. 😊