We use cookies to enhance your experience on the site
CodeWorlds

Template Literal Types - String Manipulation in Types

Template literal types are a TypeScript 4.1+ feature that allows creating string types using template literal syntax from JavaScript. This enables creating complex string-based types, manipulation, and transformation of string types at the type system level.

Template Literal Types Basics

1. Basic Syntax

1// Basic template literal types
2type Greeting = `Hello ${string}`;
3type Welcome = `Welcome to ${string}!`;
4
5// Usage examples
6const greeting1: Greeting = "Hello World";        // OK
7const greeting2: Greeting = "Hello TypeScript";   // OK
8// const greeting3: Greeting = "Hi World";        // Error! Must start with "Hello "
9
10// Combining literal types
11type EventName = "click" | "focus" | "blur";
12type EventHandler = `on${Capitalize<EventName>}`;
13// type EventHandler = "onClick" | "onFocus" | "onBlur"
14
15// Practical example - CSS units
16type CSSUnit = "px" | "%" | "em" | "rem" | "vh" | "vw";
17type CSSValue = `${number}${CSSUnit}`;
18
19const width: CSSValue = "100px";     // OK
20const height: CSSValue = "50vh";     // OK
21const margin: CSSValue = "2rem";     // OK
22// const padding: CSSValue = "10";   // Error! Missing unit
23
24// Nested template literals
25type BreakpointSize = "sm" | "md" | "lg" | "xl";
26type Breakpoint = `@${BreakpointSize}`;
27type ResponsiveProp<T extends string> = T | `${T}${Breakpoint}`;
28
29type Display = ResponsiveProp<"block" | "none" | "flex">;
30// "block" | "none" | "flex" | "block@sm" | "none@sm" | "flex@sm" |

2. Pattern Matching with Template Literal Types

1// Extracting parts of strings
2type ExtractId<T> = T extends `id-${infer Id}` ? Id : never;
3
4type UserId = ExtractId<"id-123">;        // "123"
5type PostId = ExtractId<"id-abc-def">;    // "abc-def"
6type Invalid = ExtractId<"user-123">;     // never
7
8// More complex pattern matching
9type ParseRoute<T> = T extends `/${infer Module}/${infer Action}`
10  ? { module: Module; action: Action }
11  : never;
12
13type Route1 = ParseRoute<"/users/create">;   // { module: "users"; action: "create" }
14type Route2 = ParseRoute<"/posts/edit">;     // { module: "posts"; action: "edit" }
15
16// Recursive parsing
17type Split<S extends string, D extends string> =
18  S extends `${infer T}${D}${infer U}`
19    ? [T, ...Split<U, D>]
20    : [S];
21
22type PathSegments = Split<"users/123/posts/456", "/">;
23// ["users", "123", "posts", "456"]
24
25// Extracting parameters from URL
26type ExtractParams<T extends string> = T extends `${infer Start}:${infer Param}/${infer Rest}`
27  ? { [K in Param]: string } & ExtractParams<Rest>
28  : T extends `${infer Start}:${infer Param}`
29  ? { [K in Param]: string }
30  : {};
31
32type RouteParams = ExtractParams<"/users/:userId/posts/:postId">;
33// { userId: string; postId: string }

3. Built-in String Manipulation Types

1// TypeScript provides built-in types for string manipulation
2type Uppercase<T extends string> = intrinsic;
3type Lowercase<T extends string> = intrinsic;
4type Capitalize<T extends string> = intrinsic;
5type Uncapitalize<T extends string> = intrinsic;
6
7// Usage examples
8type Shout = Uppercase<"hello world">;           // "HELLO WORLD"
9type Whisper = Lowercase<"HELLO WORLD">;         // "hello world"
10type Title = Capitalize<"typescript">;           // "Typescript"
11type CamelCase = Uncapitalize<"TypeScript">;    // "typeScript"
12
13// Practical application - generating event handlers
14type DOMEventName = "click" | "focus" | "blur" | "change" | "submit";
15type EventHandlerName<T extends DOMEventName> = `on${Capitalize<T>}`;
16
17type ClickHandler = EventHandlerName<"click">;   // "onClick"
18type FocusHandler = EventHandlerName<"focus">;   // "onFocus"
19
20// Automatic event handler mapping
21type EventHandlers = {
22  [E in DOMEventName as EventHandlerName<E>]: (event: Event) => void;
23};
24// {
25//   onClick: (event: Event) => void;
26//   onFocus: (event: Event) => void;
27//   onBlur: (event: Event) => void;
28//   onChange: (event: Event) => void;
29//   onSubmit: (event: Event) => void;
30// }

Advanced Patterns

1. Property Name Transformations

1// Snake case to camel case
2type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
3  ? `${T}${Capitalize<SnakeToCamelCase<U>>}`
4  : S;
5
6type CamelCased = SnakeToCamelCase<"hello_world_typescript">; // "helloWorldTypescript"
7
8// Camel case to snake case
9type CamelToSnakeCase<S extends string> = S extends `${infer T}${infer U}`
10  ? U extends Uncapitalize<U>
11    ? `${T}${CamelToSnakeCase<U>}`
12    : `${T}_${Lowercase<U>}${CamelToSnakeCase<U>}`
13  : S;
14
15type SnakeCased = CamelToSnakeCase<"helloWorldTypeScript">; // "hello_world_type_script"
16
17// Practical application - converting API response
18type SnakeToCamelCaseNested<T> = T extends object
19  ? {
20      [K in keyof T as SnakeToCamelCase<K & string>]: SnakeToCamelCaseNested<T[K]>
21    }
22  : T;
23
24interface APIResponse {
25  user_id: number;
26  first_name: string;
27  last_name: string;
28  created_at: string;
29  user_settings: {
30    email_notifications: boolean;
31    dark_mode: boolean;
32  };
33}
34
35type CamelCasedResponse = SnakeToCamelCaseNested<APIResponse>;
36// {
37//   userId: number;
38//   firstName: string;
39//   lastName: string;
40//   createdAt: string;
41//   userSettings: {
42//     emailNotifications: boolean;
43//     darkMode: boolean;
44//   };
45// }

2. Validation and Constraints

1// Email format validation
2type ValidateEmail<T extends string> = T extends `${infer Local}@${infer Domain}`
3  ? Domain extends `${infer Name}.${infer Extension}`
4    ? Extension extends "com" | "org" | "net" | "pl" | "edu"
5      ? T
6      : never
7    : never
8  : never;
9
10type ValidEmail = ValidateEmail<"user@example.com">;    // "user@example.com"
11type InvalidEmail = ValidateEmail<"invalid-email">;     // never
12
13// Phone number validation
14type ValidatePhoneNumber<T extends string> =
15  T extends `+${infer CountryCode}-${infer Rest}`
16    ? CountryCode extends `${number}`
17      ? Rest extends `${number}-${number}-${number}`
18        ? T
19        : never
20      : never
21    : never;
22
23type ValidPhone = ValidatePhoneNumber<"+48-123-456-789">;    // "+48-123-456-789"
24type InvalidPhone = ValidatePhoneNumber<"123-456-789">;      // never
25
26// Hex color validation
27type HexDigit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "A" | "B" | "C" | "D" | "E" | "F" | "a" | "b" | "c" | "d" | "e" | "f";
28type HexColor<T extends string> = T extends `#${HexDigit}${HexDigit}${HexDigit}${infer Rest}`
29  ? Rest extends `${HexDigit}${HexDigit}${HexDigit}`
30    ? T
31    : Rest extends ""
32    ? T
33    : never
34  : never;
35
36type ValidHex1 = HexColor<"#FF5733">;   // "#FF5733"
37type ValidHex2 = HexColor<"#F53">;      // "#F53"
38type InvalidHex = HexColor<"#GG5733">;  // never

3. Dynamic Type Generation

1// Routing system with type safety
2type RouteDefinition = {
3  "/": {};
4  "/users": {};
5  "/users/:id": { id: string };
6  "/users/:id/posts": { id: string };
7  "/posts/:postId/comments/:commentId": { postId: string; commentId: string };
8};
9
10type ExtractRouteParams<T extends string> =
11  T extends keyof RouteDefinition
12    ? RouteDefinition[T]
13    : T extends `${infer Start}/:${infer Param}/${infer Rest}`
14    ? { [K in Param]: string } & ExtractRouteParams<`${Start}/${Rest}`>
15    : T extends `${infer Start}/:${infer Param}`
16    ? { [K in Param]: string }
17    : {};
18
19// Router with type safety
20class TypedRouter<Routes extends Record<string, any>> {
21  navigate<T extends keyof Routes & string>(
22    path: T,
23    params: Routes[T]
24  ): void {
25    console.log(`Navigating to ${path} with params:`, params);
26  }
27}
28
29const router = new TypedRouter<RouteDefinition>();
30
31router.navigate("/", {});                                          // OK
32router.navigate("/users/:id", { id: "123" });                     // OK
33router.navigate("/posts/:postId/comments/:commentId", {           // OK
34  postId: "456",
35  commentId: "789"
36});
37// router.navigate("/users/:id", {});                             // Error! Missing id
38// router.navigate("/users/:id", { id: "123", extra: "field" }); // Error! Extra field
39
40// Generating SQL queries
41type Table = "users" | "posts" | "comments";
42type Column<T extends Table> =
43  T extends "users" ? "id" | "name" | "email" | "created_at" :
44  T extends "posts" ? "id" | "title" | "content" | "author_id" :
45  T extends "comments" ? "id" | "text" | "post_id" | "user_id" :
46  never;
47
48type SelectQuery<T extends Table> = `SELECT ${Column<T> | "*"} FROM ${T}`;
49type WhereClause<T extends Table> = `WHERE ${Column<T>} = ?`;
50type FullQuery<T extends Table> = `${SelectQuery<T>} ${WhereClause<T>}`;
51
52type UserQuery = SelectQuery<"users">;
53// "SELECT id FROM users" | "SELECT name FROM users" | ... | "SELECT * FROM users"
54
55type PostQueryWithWhere = FullQuery<"posts">;
56// "SELECT id FROM posts WHERE id = ?" | "SELECT title FROM posts WHERE title = ?" |

Practical Applications

1. CSS-in-JS with Type Safety

1// Design token system
2type Color = "primary" | "secondary" | "success" | "danger" | "warning";
3type Shade = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
4type ColorToken = `color-${Color}-${Shade}`;
5
6type Spacing = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16 | 20 | 24 | 32;
7type SpacingToken = `spacing-${Spacing}`;
8
9type DesignToken = ColorToken | SpacingToken;
10
11// Helper function with type safety
12function token(tokenName: DesignToken): string {
13  return `var(--${tokenName})`;
14}
15
16const primaryColor = token("color-primary-500");    // OK
17const spacing = token("spacing-4");                 // OK
18// const invalid = token("color-invalid-999");      // Error!
19
20// Responsive props system
21type Breakpoint = "sm" | "md" | "lg" | "xl";
22type ResponsiveValue<T> = T | { [K in Breakpoint]?: T };
23
24type FlexDirection = "row" | "column" | "row-reverse" | "column-reverse";
25type JustifyContent = "start" | "end" | "center" | "between" | "around" | "evenly";
26
27interface FlexProps {
28  direction?: ResponsiveValue<FlexDirection>;
29  justify?: ResponsiveValue<JustifyContent>;
30  gap?: ResponsiveValue<Spacing>;
31}
32
33// Generating CSS classes
34function generateClasses<T extends string>(
35  prefix: string,
36  value: ResponsiveValue<T>
37): string[] {
38  if (typeof value === "object") {
39    return Object.entries(value).map(([breakpoint, val]) =>
40      `${prefix}-${val}@${breakpoint}`
41    );
42  }
43  return [`${prefix}-${value}`];
44}

2. API Client with Type Safety

1// API endpoint definitions
2type APIEndpoints = {
3  "GET /users": { response: User[]; params: {} };
4  "GET /users/:id": { response: User; params: { id: string } };
5  "POST /users": { response: User; params: {}; body: CreateUserDto };
6  "PUT /users/:id": { response: User; params: { id: string }; body: UpdateUserDto };
7  "DELETE /users/:id": { response: void; params: { id: string } };
8};
9
10// Extracting method and path
11type ExtractMethod<T> = T extends `${infer Method} ${infer Path}` ? Method : never;
12type ExtractPath<T> = T extends `${infer Method} ${infer Path}` ? Path : never;
13
14// Type-safe API client
15class APIClient {
16  async request<T extends keyof APIEndpoints>(
17    endpoint: T,
18    options: {
19      params?: APIEndpoints[T]["params"];
20      body?: "body" extends keyof APIEndpoints[T] ? APIEndpoints[T]["body"] : never;
21    } = {}
22  ): Promise<APIEndpoints[T]["response"]> {
23    const method = endpoint.split(" ")[0];
24    const path = endpoint.split(" ")[1];
25
26    // Replace parameters in path
27    let url = path;
28    if (options.params) {
29      Object.entries(options.params).forEach(([key, value]) => {
30        url = url.replace(`:${key}`, value as string);
31      });
32    }
33
34    console.log(`${method} ${url}`, options.body);
35
36    // API call simulation
37    return {} as APIEndpoints[T]["response"];
38  }
39}
40
41// Usage
42const api = new APIClient();
43
44api.request("GET /users");                                    // OK
45api.request("GET /users/:id", { params: { id: "123" } });   // OK
46api.request("POST /users", { body: { /* ... */ } });        // OK
47// api.request("GET /users/:id");                            // Error! Missing params
48// api.request("GET /users", { body: {} });                  // Error! GET has no body

3. Internationalization with Type Safety

1// Translation system with nested keys
2type TranslationKeys = {
3  common: {
4    yes: string;
5    no: string;
6    cancel: string;
7  };
8  user: {
9    profile: {
10      title: string;
11      edit: string;
12    };
13    settings: {
14      title: string;
15      notifications: {
16        email: string;
17        push: string;
18      };
19    };
20  };
21};
22
23// Generating paths to keys
24type PathsToStringProps<T> = T extends string
25  ? []
26  : {
27      [K in keyof T]: [K, ...PathsToStringProps<T[K]>];
28    }[keyof T];
29
30type TranslationPath = PathsToStringProps<TranslationKeys>;
31// ["common", "yes"] | ["common", "no"] | ["user", "profile", "title"] |
32// Converting array to dot-separated string
33type JoinPath<T extends string[]> = T extends []
34  ? ""
35  : T extends [infer First]
36  ? First
37  : T extends [infer First, ...infer Rest]
38  ? First extends string
39    ? Rest extends string[]
40      ? `${First}.${JoinPath<Rest>}`
41      : never
42    : never
43  : never;
44
45type TranslationKey = JoinPath<TranslationPath>;
46// "common.yes" | "common.no" | "user.profile.title" |
47// Translation function with type safety
48function t(key: TranslationKey): string {
49  // Translation retrieval implementation
50  return key;
51}
52
53const title = t("user.profile.title");           // OK
54const email = t("user.settings.notifications.email"); // OK
55// const invalid = t("user.invalid.key");        // Error!
56
57// Translations with parameters
58type TranslationWithParams = {
59  "welcome": { name: string };
60  "items_count": { count: number };
61  "date_format": { date: Date };
62};
63
64function tWithParams<K extends keyof TranslationWithParams>(
65  key: K,
66  params: TranslationWithParams[K]
67): string {
68  // Implementation with parameter substitution
69  return `${key} with ${JSON.stringify(params)}`;
70}
71
72tWithParams("welcome", { name: "Jan" });           // OK
73tWithParams("items_count", { count: 5 });          // OK
74// tWithParams("welcome", { count: 5 });           // Error! Invalid parameters

Template literal types are a powerful tool that brings string manipulation to the type system level. This enables creating more expressive and safer APIs, especially in the context of routing, internationalization, and styling systems.

Go to CodeWorlds