Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Template literal types - manipulacja stringami w typach

Template literal types to funkcja TypeScript 4.1+, która pozwala na tworzenie typów stringowych używając składni template literals z JavaScript. Umożliwia to tworzenie złożonych typów opartych na stringach, manipulację i transformację typów stringowych na poziomie systemu typów.

Podstawy template literal types

1. Składnia podstawowa

1// Podstawowe template literal types
2type Greeting = `Hello ${string}`;
3type Welcome = `Welcome to ${string}!`;
4
5// Przykłady użycia
6const greeting1: Greeting = "Hello World";        // OK
7const greeting2: Greeting = "Hello TypeScript";   // OK
8// const greeting3: Greeting = "Hi World";        // Błąd! Musi zaczynać się od "Hello "
9
10// Łączenie literal types
11type EventName = "click" | "focus" | "blur";
12type EventHandler = `on${Capitalize<EventName>}`;
13// type EventHandler = "onClick" | "onFocus" | "onBlur"
14
15// Praktyczny przykład - 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";   // Błąd! Brakuje jednostki
23
24// Zagnieżdżone 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 z template literal types

1// Wyciąganie części stringów
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// Bardziej złożone 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// Rekursywne parsowanie
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// Wyciąganie parametrów z 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. Wbudowane string manipulation types

1// TypeScript udostępnia wbudowane typy do manipulacji stringami
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// Przykłady użycia
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// Praktyczne zastosowanie - generowanie 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// Automatyczne mapowanie event handlers
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// }

Zaawansowane wzorce

1. Transformacje nazw właściwości

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// Praktyczne zastosowanie - konwersja 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. Walidacja i ograniczenia

1// Walidacja formatu email
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// Walidacja numerów telefonów
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// Walidacja hex color
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. Dynamiczne generowanie typów

1// System routingu z 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 z 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", {});                             // Błąd! Brakuje id
38// router.navigate("/users/:id", { id: "123", extra: "field" }); // Błąd! Dodatkowe pole
39
40// Generowanie 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 = ?" |

Praktyczne zastosowania

1. CSS-in-JS z type safety

1// System design tokens
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// Funkcja pomocnicza z 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");      // Błąd!
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// Generowanie klas CSS
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 z type safety

1// Definicja endpointów API
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// Wyciąganie metody i ścieżki
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    // Zastąp parametry w ścieżce
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    // Symulacja API call
37    return {} as APIEndpoints[T]["response"];
38  }
39}
40
41// Użycie
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");                            // Błąd! Brakuje params
48// api.request("GET /users", { body: {} });                  // Błąd! GET nie ma body

3. Internacjonalizacja z type safety

1// System tłumaczeń z zagnieżdżonymi kluczami
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// Generowanie ścieżek do kluczy
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// Konwersja tablicy do stringa z kropkami
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// Funkcja tłumaczenia z type safety
48function t(key: TranslationKey): string {
49  // Implementacja pobierania tłumaczenia
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");        // Błąd!
56
57// Tłumaczenia z parametrami
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  // Implementacja z podstawianiem parametrów
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 });           // Błąd! Nieprawidłowe parametry

Template literal types to potężne narzędzie, które przenosi manipulację stringami na poziom systemu typów. Pozwala to na tworzenie bardziej ekspresywnych i bezpiecznych API, szczególnie w kontekście routingu, internacjonalizacji i systemów stylowania.

Przejdź do CodeWorlds