Kurs JavaScript i TypeScript · Moduł 10: TypeScript w praktyce

Template literal types - manipulacja stringami w typach

13 min czytania
W tej lekcji3

Wyobraź sobie system etykiet próbek DNA, w którym każda nazwa ma format dna-gatunek-numer. Zwykły string przepuści każdą literówkę, a ręczne wypisanie wszystkich wariantów zajęłoby tygodnie. Template literal types to funkcja TypeScript 4.1+, która pozwala tworzyć typy stringowe składnią template literals z JavaScript. Kompilator sprawdza wtedy format napisu, zanim kod trafi do laboratorium.

Podstawy template literal types

Składnia podstawowa

Zapis przypomina zwykły template literal, ale w miejscu wartości stoi typ: ${string} oznacza dowolny napis, a ${number} dowolną liczbę zapisaną tekstem:

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" |

Greeting akceptuje każdy napis zaczynający się od Hello , a nie dosłownie tekst ze znakiem dolara. Gdy wstawisz unię, TypeScript wygeneruje unię wszystkich kombinacji, więc EventHandler to trzy nazwy, a Display aż piętnaście wariantów.

Pattern matching z template literal types

Połączenie z typami warunkowymi i słowem infer pozwala rozbierać napisy na części, podobnie jak wyrażenie regularne, tylko w czasie kompilacji:

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 }

infer Id łapie resztę napisu po prefiksie id-, a Split wywołuje sam siebie, aż skończą się separatory. Rekurencja w typach ma jednak limit głębokości, więc bardzo długie napisy mogą przerosnąć kompilator.

Wbudowane string manipulation types

TypeScript ma cztery wbudowane typy do zmiany wielkości liter: Uppercase, Lowercase, Capitalize i Uncapitalize. Nie ma na przykład typu Reverse, taki musiałbyś napisać sam:

1// TypeScript udostępnia wbudowane typy do manipulacji stringami
2// Tak są zadeklarowane w lib.es5.d.ts (nie deklaruj ich ponownie, bo dostaniesz Duplicate identifier):
3// type Uppercase<T extends string> = intrinsic;
4// type Lowercase<T extends string> = intrinsic;
5// type Capitalize<T extends string> = intrinsic;
6// type Uncapitalize<T extends string> = intrinsic;
7
8// Przykłady użycia
9type Shout = Uppercase<"hello world">;           // "HELLO WORLD"
10type Whisper = Lowercase<"HELLO WORLD">;         // "hello world"
11type Title = Capitalize<"typescript">;           // "Typescript"
12type CamelCase = Uncapitalize<"TypeScript">;    // "typeScript"
13
14// Praktyczne zastosowanie - generowanie event handlers
15type DOMEventName = "click" | "focus" | "blur" | "change" | "submit";
16type EventHandlerName<T extends DOMEventName> = `on${Capitalize<T>}`;
17
18type ClickHandler = EventHandlerName<"click">;   // "onClick"
19type FocusHandler = EventHandlerName<"focus">;   // "onFocus"
20
21// Automatyczne mapowanie event handlers
22type EventHandlers = {
23  [E in DOMEventName as EventHandlerName<E>]: (event: Event) => void;
24};
25// {
26//   onClick: (event: Event) => void;
27//   onFocus: (event: Event) => void;
28//   onBlur: (event: Event) => void;
29//   onChange: (event: Event) => void;
30//   onSubmit: (event: Event) => void;
31// }

Słowo intrinsic oznacza, że implementację dostarcza kompilator, dlatego deklaracje są w komentarzu, a Ty tylko z nich korzystasz. Klauzula as w typie mapowanym zmienia nazwy kluczy, więc z listy zdarzeń powstaje obiekt z polami onClick, onFocus i kolejnymi.

Zaawansowane wzorce

Transformacje nazw właściwości

API często zwraca pola w stylu snake_case, a w kodzie wolisz camelCase. Typy mogą przeliczyć nazwy za Ciebie:

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 C}${infer Rest}`
10  ? C extends Lowercase<C>
11    ? `${C}${CamelToSnakeCase<Rest>}`
12    : `_${Lowercase<C>}${CamelToSnakeCase<Rest>}`
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// }

CamelToSnakeCase sprawdza znak po znaku i każdą wielką literę zamienia na podkreślnik z małą literą. To tylko typy: dane w runtime przekształcasz osobną funkcją, a typ jedynie opisuje wynik.

Walidacja i ograniczenia

Typy potrafią też sprawdzać format napisów. Tak wygląda walidacja adresu email, numeru telefonu i koloru szesnastkowego:

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

Nieprawidłowy napis daje never, więc funkcja przyjmująca taki typ go odrzuci. Działa to dla literałów znanych w czasie kompilacji, a tekst wpisany przez użytkownika nadal sprawdzasz w runtime.

Dynamiczne generowanie typów

Mapa tras opisana typem daje routerowi kontrolę nad parametrami, a unie tabel i kolumn generują poprawne zapytania SQL:

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 = ?" |

navigate wymaga dokładnie takich parametrów, jakie opisuje trasa, więc brakujące albo nadmiarowe pole to błąd kompilacji. Uważaj na rozmiar unii: iloczyn kilku dużych unii rośnie lawinowo, a TypeScript odmawia pracy przy uniach liczących ponad 100 000 elementów.

Praktyczne zastosowania

CSS-in-JS z type safety

System design tokenów to klasyczne zastosowanie: nazwy powstają z połączenia koloru i odcienia, więc literówka nie przejdzie:

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}

Pięć kolorów i dziewięć odcieni daje 45 nazw, których nie wypisujesz ręcznie.

API client z type safety

Klucze w postaci "METODA /ścieżka" opisują endpointy, a klient wyprowadza z nich typ odpowiedzi i ciała żądania:

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");                            // Kompiluje się! params jest opcjonalne
48// api.request("GET /users", { body: {} });                  // Błąd! GET nie ma body

Wywołanie GET /users z ciałem to błąd, bo body ma wtedy typ never. Parametry są jednak opcjonalne, więc GET /users/:id bez params się skompiluje, a wymuszenie ich wymagałoby przeciążeń albo typu warunkowego dla całych opcji.

Internacjonalizacja z type safety

Na koniec klucze tłumaczeń: z zagnieżdżonego obiektu powstaje unia ścieżek w stylu user.profile.title:

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> & string}`
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

Zapis JoinPath<Rest> & string mówi kompilatorowi, że wynik rekurencji jest napisem. Bez tego wstawienie go do szablonu kończy się błędem typu.

Template literal types to potężne narzędzie, które przenosi manipulację stringami na poziom systemu typów. Pozwala to tworzyć bardziej ekspresywne i bezpieczne API, szczególnie w routingu, internacjonalizacji i systemach stylowania. Moja rada: używaj ich tam, gdzie format napisu jest kontraktem, na przykład w nazwach zdarzeń i tokenów, a nie do walidowania danych od użytkownika. W edytorze poniżej poeksperymentujesz z przykładami, a w kolejnej lekcji wykorzystasz tę wiedzę w typowanym systemie zdarzeń.

Pamiętaj: template literal type to szablon etykiety na probówce, który nie wpuści do laboratorium próbki opisanej w złym formacie.

Kod do tej lekcji: index.ts
1// Przykłady template literal types w TypeScript
2
3// 1. Podstawowe template literal types
4type Greeting = `Hello ${string}`;
5type ErrorMessage = `Error: ${string}`;
6
7const greeting: Greeting = "Hello TypeScript!";      // OK
8const error: ErrorMessage = "Error: File not found"; // OK
9// const invalid: Greeting = "Hi there!";            // Błąd!
10
11// 2. CSS wartości z type safety
12type CSSUnit = "px" | "%" | "em" | "rem" | "vh" | "vw";
13type CSSValue = `${number}${CSSUnit}`;
14
15const width: CSSValue = "100px";
16const height: CSSValue = "50vh";
17const margin: CSSValue = "2.5rem";
18
19// 3. Event handlers - automatyczne generowanie nazw
20type EventName = "click" | "focus" | "blur" | "change";
21type EventHandlerName<T extends EventName> = `on${Capitalize<T>}`;
22
23type ClickHandler = EventHandlerName<"click">;  // "onClick"
24type FocusHandler = EventHandlerName<"focus">;  // "onFocus"
25
26// Mapowanie event handlers
27type EventHandlers = {
28  [E in EventName as EventHandlerName<E>]: (event: Event) => void;
29};
30
31// 4. Snake case to camel case
32type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
33  ? `${T}${Capitalize<SnakeToCamelCase<U>>}`
34  : S;
35
36// Konwersja właściwości obiektu
37type SnakeToCamelCaseNested<T> = T extends object
38  ? {
39      [K in keyof T as SnakeToCamelCase<K & string>]: SnakeToCamelCaseNested<T[K]>
40    }
41  : T;
42
43// API response transformation
44interface APIUserResponse {
45  user_id: number;
46  first_name: string;
47  last_name: string;
48  email_address: string;
49  created_at: string;
50}
51
52type CamelCasedUser = SnakeToCamelCaseNested<APIUserResponse>;
53// Wynik: { userId, firstName, lastName, emailAddress, createdAt }
54
55// 5. Routing z type safety
56type Route<T extends string> = T extends `/users/${infer UserId}/posts/${infer PostId}`
57  ? { userId: UserId; postId: PostId }
58  : T extends `/users/${infer UserId}`
59  ? { userId: UserId }
60  : T extends `/posts/${infer PostId}`
61  ? { postId: PostId }
62  : never;
63
64// Test routingu
65type UserPostRoute = Route<"/users/123/posts/456">; // { userId: "123"; postId: "456" }
66type UserRoute = Route<"/users/789">;               // { userId: "789" }
67type PostRoute = Route<"/posts/101">;               // { postId: "101" }
68
69// 6. Praktyczny system tłumaczeń
70type TranslationKey =
71  | "common.yes"
72  | "common.no"
73  | "common.cancel"
74  | "user.profile.title"
75  | "user.profile.edit"
76  | "errors.notFound"
77  | "errors.unauthorized";
78
79// Funkcja tłumaczenia z type safety
80function t(key: TranslationKey): string {
81  const translations: Record<TranslationKey, string> = {
82    "common.yes": "Tak",
83    "common.no": "Nie",
84    "common.cancel": "Anuluj",
85    "user.profile.title": "Profil użytkownika",
86    "user.profile.edit": "Edytuj profil",
87    "errors.notFound": "Nie znaleziono",
88    "errors.unauthorized": "Brak autoryzacji"
89  };
90
91  return translations[key];
92}
93
94// Użycie z pełnym type safety
95console.log(t("common.yes"));              // "Tak"
96console.log(t("user.profile.title"));      // "Profil użytkownika"
97// console.log(t("invalid.key"));          // Błąd kompilacji!
98
99// 7. CSS-in-JS design tokens
100type Color = "primary" | "secondary" | "success" | "danger";
101type Shade = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
102type ColorToken = `color-${Color}-${Shade}`;
103
104type Spacing = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12;
105type SpacingToken = `spacing-${Spacing}`;
106
107type DesignToken = ColorToken | SpacingToken;
108
109function token(name: DesignToken): string {
110  return `var(--${name})`;
111}
112
113// Użycie design tokens
114const primaryColor = token("color-primary-500");
115const dangerColor = token("color-danger-700");
116const smallSpace = token("spacing-2");
117const largeSpace = token("spacing-8");
118
119console.log("Design tokens:", {
120  primaryColor,
121  dangerColor,
122  smallSpace,
123  largeSpace
124});
125
126// 8. Walidacja formatu
127type Email<T extends string> = T extends `${string}@${string}.${string}` ? T : never;
128type ValidEmail = Email<"user@example.com">;     // "user@example.com"
129type InvalidEmail = Email<"not-an-email">;       // never
130
131// Funkcja z walidacją email
132function sendEmail<T extends string>(email: Email<T>, subject: string): void {
133  console.log(`Wysyłam email do ${email} z tematem: ${subject}`);
134}
135
136sendEmail("user@example.com" as Email<"user@example.com">, "Witaj!");
137// sendEmail("invalid", "Test");  // Błąd kompilacji!

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Co tworzy typ type Greeting = Hello ${string} w TypeScript?

  2. 2. Który z poniższych NIE jest wbudowanym string manipulation type w TypeScript?

Zadania praktyczne w grze

  • Edytor kodu

    Stwórz typy oparte na template literal types.

  • Klikanie w kolejności

    Ułóż elementy template literal type:

  • Układanie w pionie

    Ułóż etapy tworzenia template literal type:

  • Układanie w poziomie

    Ułóż elementy template literal type w odpowiedniej kolejności:

  • Edytor kodu

    Stwórz type do parsowania URL paths.

Przydatne artykuły