Template Literal Type은 JavaScript의 템플릿 리터럴 문법을 타입 레벨 에서 사용하는 것입니다.

기본 문법

TYPESCRIPT
// 문자열 리터럴을 조합해서 새 타입 생성
type Greeting = `hello ${string}`;

const a: Greeting = 'hello world';   // OK
const b: Greeting = 'hello TypeScript'; // OK
// const c: Greeting = 'hi world';    // ❌ Error

// 유니온과 조합하면 모든 경우의 수가 생성됨
type Color = 'red' | 'blue' | 'green';
type Size = 'small' | 'large';

type ColorSize = `${Color}-${Size}`;
// 'red-small' | 'red-large' | 'blue-small' | 'blue-large' | 'green-small' | 'green-large'

유니온끼리 조합하면 곱집합 이 만들어집니다. 3가지 색상 x 2가지 크기 = 6가지 조합.

내장 문자열 변환 유틸리티

TYPESCRIPT
type Upper = Uppercase<'hello'>;      // 'HELLO'
type Lower = Lowercase<'HELLO'>;      // 'hello'
type Cap = Capitalize<'hello'>;       // 'Hello'
type Uncap = Uncapitalize<'Hello'>;   // 'hello'

// 유니온에도 분배됨
type Events = 'click' | 'scroll' | 'keypress';
type OnEvents = `on${Capitalize<Events>}`;
// 'onClick' | 'onScroll' | 'onKeypress'

실전 패턴

CSS 속성 타입

TYPESCRIPT
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;

const width: CSSValue = '100px';    // OK
const height: CSSValue = '50vh';    // OK
// const wrong: CSSValue = '100';    // ❌ Error — 단위 없음
// const wrong2: CSSValue = 'auto';  // ❌ Error — 패턴 불일치

이벤트 핸들러 이름 생성

TYPESCRIPT
type EventName = 'click' | 'focus' | 'blur' | 'change';

// on + 대문자 시작 이벤트 이름
type EventHandler = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur' | 'onChange'

// 핸들러 맵 생성
type EventHandlerMap = {
  [K in EventName as `on${Capitalize<K>}`]: (event: Event) => void;
};
// {
//   onClick: (event: Event) => void;
//   onFocus: (event: Event) => void;
//   onBlur: (event: Event) => void;
//   onChange: (event: Event) => void;
// }

API 라우트 타입

TYPESCRIPT
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/posts' | '/comments';

type ApiRoute = `${HttpMethod} ${Endpoint}`;
// 'GET /users' | 'GET /posts' | 'GET /comments' |
// 'POST /users' | 'POST /posts' | 'POST /comments' | ...

infer와 조합 — 문자열 파싱

TYPESCRIPT
// 점 표기법에서 첫 번째 키 추출
type FirstKey<S extends string> = S extends `${infer K}.${string}` ? K : S;

type A = FirstKey<'user.name.first'>; // 'user'
type B = FirstKey<'name'>;             // 'name'

// 점 표기법에서 나머지 추출
type RestKeys<S extends string> = S extends `${string}.${infer R}` ? R : never;

type C = RestKeys<'user.name.first'>; // 'name.first'

깊은 속성 접근 타입

TYPESCRIPT
// 점 표기법으로 깊은 속성 타입을 추출
type DeepGet<T, Path extends string> =
  Path extends `${infer K}.${infer Rest}`
    ? K extends keyof T
      ? DeepGet<T[K], Rest>
      : never
    : Path extends keyof T
      ? T[Path]
      : never;

interface Config {
  db: {
    host: string;
    port: number;
  };
  app: {
    name: string;
  };
}

type DbHost = DeepGet<Config, 'db.host'>; // string
type AppName = DeepGet<Config, 'app.name'>; // string

카멜케이스를 케밥케이스로 변환

TYPESCRIPT
type CamelToKebab<S extends string> =
  S extends `${infer C}${infer Rest}`
    ? C extends Uppercase<C>
      ? `-${Lowercase<C>}${CamelToKebab<Rest>}`
      : `${C}${CamelToKebab<Rest>}`
    : S;

type A = CamelToKebab<'backgroundColor'>; // 'background-color'
type B = CamelToKebab<'fontSize'>;         // 'font-size'

공부하다 보니 타입 레벨에서 문자열을 파싱하는 게 꽤 강력하더라고요. 타입 챌린지에서도 자주 출제되는 유형입니다.

Replace 타입

TYPESCRIPT
// 첫 번째 매칭만 교체
type Replace<
  S extends string,
  From extends string,
  To extends string
> = From extends ''
  ? S
  : S extends `${infer Before}${From}${infer After}`
    ? `${Before}${To}${After}`
    : S;

type A = Replace<'hello world', 'world', 'TS'>; // 'hello TS'

// 모든 매칭 교체
type ReplaceAll<
  S extends string,
  From extends string,
  To extends string
> = From extends ''
  ? S
  : S extends `${infer Before}${From}${infer After}`
    ? ReplaceAll<`${Before}${To}${After}`, From, To>
    : S;

정리

  • Template Literal Type은 문자열 패턴을 타입으로 표현한다
  • 유니온과 조합하면 모든 경우의 수(곱집합)가 생성된다
  • Uppercase, Lowercase, Capitalize, Uncapitalize 내장 유틸리티가 있다
  • infer와 조합하면 문자열을 파싱하고 변환할 수 있다
  • CSS 값, 이벤트 핸들러, API 라우트 등 실전에서 다양하게 활용된다
댓글 로딩 중...