본문으로 건너뛰기
김신건의 로그

[TypeScript] Template Literal Types

· 수정 · 📖 약 1분 · 380자/단어 #typescript #types #template-literal #string-manipulation
TypeScript Template Literal Types, TS Template Literal Types, TypeScript string manipulation, Uppercase, Lowercase, Capitalize, Uncapitalize, template string types

정의

Template Literal Type 은 TypeScript 4.1 에서 도입된 문법으로, JavaScript template literal 처럼 백틱과 ${...} 를 이용해 타입 레벨 에서 문자열을 조합/조작합니다. Union 분배, 내장 문자열 utility, mapped type / conditional type 조합으로 강력한 정적 계산이 가능합니다.

기본

type World = "world";
type Greeting = `hello ${World}`;   // "hello world"

type Cardinal = "north" | "south" | "east" | "west";
type Quadrant = `${Cardinal}-${Cardinal}`;
// = "north-north" | "north-south" | ... 총 16개

Union 안에서 자동 분배 (cartesian product).

내장 문자열 utility

  • Uppercase<S>
  • Lowercase<S>
  • Capitalize<S>: 첫 글자 대문자
  • Uncapitalize<S>: 첫 글자 소문자
type A = Uppercase<"hello">;      // "HELLO"
type B = Lowercase<"HELLO">;       // "hello"
type C = Capitalize<"hello">;      // "Hello"
type D = Uncapitalize<"Hello">;    // "hello"

Event Handler 이름

type EventName<T extends string> = `on${Capitalize<T>}`;

type A = EventName<"click">;       // "onClick"
type B = EventName<"mouseover">;    // "onMouseover"

Mapped 와 조합:

type EventHandlers<E extends string> = {
  [K in E as `on${Capitalize<K>}`]: (e: Event) => void;
};

type Handlers = EventHandlers<"click" | "hover" | "submit">;
// = {onClick: (e: Event) => void; onHover: ...; onSubmit: ...}

URL / Route 타이핑

type Route = `/users/${string}` | `/posts/${number}`;

const r1: Route = "/users/kim";        // OK
const r2: Route = "/posts/42";          // OK
const r3: Route = "/comments/1";        // 오류

Path parameter 추출

type ExtractParams<S extends string> =
  S extends `${string}:${infer P}/${infer R}`
    ? P | ExtractParams<R>
    : S extends `${string}:${infer P}`
    ? P
    : never;

type A = ExtractParams<"/users/:id/posts/:postId">;
// = "id" | "postId"

type RouteParams<S extends string> = {
  [K in ExtractParams<S>]: string;
};

type P = RouteParams<"/users/:id/posts/:postId">;
// = {id: string; postId: string}

Type-safe routing 의 기반.

Case conversion

SnakeToCamel

type SnakeToCamel<S extends string> =
  S extends `${infer H}_${infer R}`
    ? `${H}${Capitalize<SnakeToCamel<R>>}`
    : S;

type A = SnakeToCamel<"user_id">;           // "userId"
type B = SnakeToCamel<"created_at_utc">;     // "createdAtUtc"

CamelToSnake

type CamelToSnake<S extends string> =
  S extends `${infer H}${infer T}`
    ? T extends Uncapitalize<T>
      ? `${Uncapitalize<H>}${CamelToSnake<T>}`
      : `${Uncapitalize<H>}_${CamelToSnake<T>}`
    : S;

type A = CamelToSnake<"userId">;     // "user_id"
type B = CamelToSnake<"getName">;     // "get_name"

KebabCase, ScreamingSnake 등

type-fest 라이브러리에 이미 정의되어 있음. 재발명하기보다는 lookup.

Numeric parsing

type ParseInt<S extends string> =
  S extends `${infer N extends number}` ? N : never;

type A = ParseInt<"42">;    // 42
type B = ParseInt<"3.14">;   // 3.14
type C = ParseInt<"foo">;    // never

TypeScript 4.8+ 에서 ${infer N extends number} 문법으로 문자열을 숫자로 파싱 가능.

String Split

type Split<S extends string, D extends string> =
  S extends `${infer H}${D}${infer R}` ? [H, ...Split<R, D>] : [S];

type A = Split<"a,b,c", ",">;    // ["a", "b", "c"]
type B = Split<"1.2.3", ".">;    // ["1", "2", "3"]

String Join

type Join<T extends string[], D extends string> =
  T extends [] ? "" :
  T extends [infer F extends string] ? F :
  T extends [infer F extends string, ...infer R extends string[]]
    ? `${F}${D}${Join<R, D>}`
    : never;

type A = Join<["a", "b", "c"], "-">;   // "a-b-c"

실전: CSS unit

type CssUnit = "px" | "em" | "rem" | "%" | "vh" | "vw";
type CssValue = `${number}${CssUnit}` | `${number}` | "auto" | "inherit";

const v1: CssValue = "10px";      // OK
const v2: CssValue = "auto";       // OK
const v3: CssValue = "10bar";      // 오류

실전: Environment variable

type EnvKey = `NEXT_PUBLIC_${string}`;

function getEnv<K extends EnvKey>(key: K): string | undefined {
  return process.env[key];
}

getEnv("NEXT_PUBLIC_API_URL");   // OK
getEnv("DATABASE_URL");           // 오류

실전: SQL 유사

type SqlWhere<T> = {
  [K in keyof T]?: T[K] | `${string}${T[K] extends string ? "LIKE" : ""}${string}`;
};

// ... 실용성 낮음, 예시 정도

Distributive with template literal

type Prefix<S extends string> = `prefix_${S}`;

type A = Prefix<"a" | "b" | "c">;   // "prefix_a" | "prefix_b" | "prefix_c"

Union 이 자동 분배되어 각 요소에 대해 template 적용.

Cartesian product

type Combinations<A extends string, B extends string> = `${A}-${B}`;

type A = Combinations<"foo" | "bar", "1" | "2">;
// = "foo-1" | "foo-2" | "bar-1" | "bar-2"

함정

WARNING

재귀 depth 제한 (50 정도). 매우 긴 문자열 파싱은 실패.

CAUTION

${string} 은 모든 문자열 매칭. Loose 함. ${number} 로 명시하는 편이 안전.

WARNING

Capitalize<T> 는 T 가 string 이어야 함. Capitalize<string & K> 로 좁힘.

IMPORTANT

성능. 대규모 template literal type 은 컴파일 시간 급증. Alias 로 캐시.

CAUTION

Runtime 에는 없음. Template literal type 은 컴파일 시만. 실제 문자열 조작은 JavaScript 로.

관련 위키

이 글의 용어 (7개)
[TypeScript] Conditional Typestypescript
정의 Conditional Type 은 형태로 타입 레벨에서 조건 분기를 표현합니다. 키워드로 매칭된 타입을 추출하고, union 에 대해서는 자동 분배 (distributive…
[TypeScript] Genericstypescript
정의 Generics 는 타입을 파라미터화하는 문법입니다. 함수, 클래스, 인터페이스, type alias 가 재사용 가능 하면서도 타입 안전 하도록 만듭니다. Java 의 ge…
[TypeScript] Mapped Typestypescript
정의 Mapped Type 은 기존 타입의 각 필드를 순회 ( ) 하며 새 타입을 생성하는 문법입니다. , modifier 를 붙이거나 제거 ( , ) 하고, clause 로 키…
[TypeScript] Type Aliasestypescript
정의 Type Alias 는 타입에 이름을 붙이는 TypeScript 문법 ( ). Interface 는 객체 shape 만 표현하지만, type alias 는 모든 타입 표현식…
[TypeScript] Type Narrowing (Type Guards)typescript
정의 Type Narrowing 은 TypeScript 컴파일러가 제어 흐름 분석 (control flow analysis) 을 통해 특정 위치에서 값의 타입을 더 좁게 추론하는…
[TypeScript] Utility Typestypescript
정의 Utility Types 는 TypeScript 표준 라이브러리에 포함된 미리 정의된 generic 타입 들입니다. 기존 타입을 변형/구성하는 흔한 패턴을 제공하며, map…
TypeScripttypescript
정의 TypeScript 는 Microsoft 가 2012년 발표한 JavaScript 의 상위 집합 프로그래밍 언어입니다. 정적 타입 시스템, 인터페이스, 제네릭, enum, …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기