Kurs JavaScript i TypeScript · Moduł 10: TypeScript w praktyce
Module augmentation i declaration merging
W tej lekcji3
Wyobraź sobie, że park kupił gotowy system kamer, a Ty chcesz dopisać do jego raportów pole dinosaurId. Nie możesz edytować kodu dostawcy, bo zniknie przy następnej aktualizacji. Module augmentation i declaration merging to zaawansowane funkcje TypeScript, które pozwalają rozszerzać istniejące moduły i łączyć deklaracje bez ruszania oryginału. Te mechanizmy są kluczowe przy pracy z bibliotekami zewnętrznymi, tworzeniu pluginów i rozszerzaniu frameworków.
Declaration Merging
Declaration merging (łączenie deklaracji) oznacza, że kompilator skleja kilka deklaracji o tej samej nazwie w jedną definicję. Dotyczy to interfejsów, przestrzeni nazw (namespace) oraz par: klasa z namespace, funkcja z namespace i enum z namespace. Nie łączą się natomiast aliasy typów (type) ani dwie klasy o tej samej nazwie.
Podstawy declaration merging
Najczęstszy przypadek to dwa interfejsy o tej samej nazwie. Obok widać łączenie przestrzeni nazw oraz klasy z namespace, który dokłada do klasy typy i funkcje statyczne:
1// Interface merging - najczęstszy przypadek
2interface User {
3 id: number;
4 name: string;
5}
6
7interface User {
8 email: string; // Dodajemy nową właściwość
9 age?: number; // I kolejną opcjonalną
10}
11
12// TypeScript automatycznie łączy obie deklaracje
13const user: User = {
14 id: 1,
15 name: "Jan Kowalski",
16 email: "jan@example.com"
17 // age jest opcjonalne
18};
19
20// Namespace merging
21namespace MyApp {
22 export interface Config {
23 apiUrl: string;
24 }
25
26 export function initialize(config: Config): void {
27 console.log("Inicjalizacja z:", config.apiUrl);
28 }
29}
30
31namespace MyApp {
32 export interface Config {
33 timeout?: number; // Rozszerzamy interfejs Config
34 }
35
36 export class Logger {
37 log(message: string): void {
38 console.log(`[MyApp]: ${message}`);
39 }
40 }
41}
42
43// Używamy połączonych deklaracji
44const config: MyApp.Config = {
45 apiUrl: "https://api.example.com",
46 timeout: 5000
47};
48
49const logger = new MyApp.Logger();
50logger.log("Aplikacja uruchomiona");
51
52// Class and namespace merging
53class Album {
54 constructor(public title: string, public artist: string) {}
55}
56
57namespace Album {
58 export interface Track {
59 number: number;
60 title: string;
61 duration: number;
62 }
63
64 export function createFromJSON(json: string): Album {
65 const data = JSON.parse(json);
66 return new Album(data.title, data.artist);
67 }
68}
69
70// Używamy klasy i namespace razem
71const album = new Album("The Wall", "Pink Floyd");
72const track: Album.Track = {
73 number: 1,
74 title: "In The Flesh?",
75 duration: 196
76};
77
78const albumFromJSON = Album.createFromJSON('{"title": "Dark Side", "artist": "Pink Floyd"}');Obiekt user musi mieć pola z obu deklaracji, a Album działa jednocześnie jako klasa (new Album(...)) i jako przestrzeń nazw (Album.Track). Jeśli oba interfejsy deklarują to samo pole, jego typy muszą być identyczne, inaczej kompilator zgłosi błąd.
Funkcje i namespace merging
Funkcja połączona z namespace to obiekt, który można wywołać i który ma dodatkowe pola, jak w wielu bibliotekach JavaScript. To samo działa z enumem:
1// Function and namespace merging - przydatne dla tworzenia callable objects
2function createCounter(initial: number = 0): () => number {
3 let count = initial;
4 return () => ++count;
5}
6
7namespace createCounter {
8 export interface Options {
9 initial?: number;
10 step?: number;
11 max?: number;
12 }
13
14 export function withOptions(options: Options): () => number {
15 let count = options.initial || 0;
16 const step = options.step || 1;
17 const max = options.max || Infinity;
18
19 return () => {
20 if (count + step > max) {
21 return count;
22 }
23 count += step;
24 return count;
25 };
26 }
27
28 export function reset(counter: () => number): void {
29 // Implementacja resetu
30 console.log("Counter reset");
31 }
32}
33
34// Używamy funkcji i jej namespace
35const counter1 = createCounter(10);
36const counter2 = createCounter.withOptions({ initial: 0, step: 5, max: 100 });
37
38console.log(counter1()); // 11
39console.log(counter2()); // 5
40createCounter.reset(counter1);
41
42// Enum and namespace merging
43enum Status {
44 Idle = "IDLE",
45 Loading = "LOADING",
46 Success = "SUCCESS",
47 Error = "ERROR"
48}
49
50namespace Status {
51 export function isComplete(status: Status): boolean {
52 return status === Status.Success || status === Status.Error;
53 }
54
55 export function isInProgress(status: Status): boolean {
56 return status === Status.Loading;
57 }
58
59 export interface Metadata {
60 timestamp: Date;
61 message?: string;
62 }
63}
64
65// Używamy enum z dodatkowymi funkcjami
66const currentStatus = Status.Loading;
67console.log(Status.isInProgress(currentStatus)); // true
68console.log(Status.isComplete(currentStatus)); // falsecreateCounter(10) zwraca licznik startujący od 10, a createCounter.withOptions to funkcja podpięta pod tę samą nazwę. Enum Status dostał metody pomocnicze, a jego wartości się nie zmieniły.
Merging z generics
Łączone interfejsy generyczne muszą mieć identyczne parametry typu, łącznie z nazwami. Klasa implementuje wtedy sumę wszystkich deklaracji:
1// Generic interface merging
2interface Collection<T> {
3 items: T[];
4 add(item: T): void;
5}
6
7interface Collection<T> {
8 remove(item: T): boolean;
9 find(predicate: (item: T) => boolean): T | undefined;
10}
11
12class ArrayCollection<T> implements Collection<T> {
13 items: T[] = [];
14
15 add(item: T): void {
16 this.items.push(item);
17 }
18
19 remove(item: T): boolean {
20 const index = this.items.indexOf(item);
21 if (index > -1) {
22 this.items.splice(index, 1);
23 return true;
24 }
25 return false;
26 }
27
28 find(predicate: (item: T) => boolean): T | undefined {
29 return this.items.find(predicate);
30 }
31}
32
33// Generic namespace merging
34namespace DataStructures {
35 export interface Node<T> {
36 value: T;
37 next?: Node<T>;
38 }
39
40 export class LinkedList<T> {
41 head?: Node<T>;
42
43 add(value: T): void {
44 const node: Node<T> = { value };
45 if (!this.head) {
46 this.head = node;
47 } else {
48 let current = this.head;
49 while (current.next) {
50 current = current.next;
51 }
52 current.next = node;
53 }
54 }
55 }
56}
57
58namespace DataStructures {
59 export interface Node<T> {
60 prev?: Node<T>; // Dodajemy wskaźnik do poprzedniego elementu
61 }
62
63 export class DoublyLinkedList<T> extends LinkedList<T> {
64 // Implementacja listy dwukierunkowej
65 }
66}ArrayCollection musi mieć add, remove i find, bo Collection<T> to już jeden interfejs. Druga część namespace DataStructures dokłada do węzła wskaźnik prev.
Module Augmentation
Module augmentation to rozszerzanie typów z cudzego modułu. Importujesz oryginalny moduł (co czyni plik modułem), otwierasz jego deklarację przez declare module 'nazwa', dodajesz nowe pola lub metody do interfejsu, a TypeScript sam łączy deklaracje. Dla pliku lokalnego napiszesz na przykład declare module './dinosaur' { interface Dinosaur { fly(): void } }.
Rozszerzanie modułów zewnętrznych
Dodajemy do żądania Express dane użytkownika, a do odpowiedzi metody pomocnicze. Rozszerzamy express-serve-static-core, bo stamtąd pochodzą typy req i res w handlerach:
1// Rozszerzanie typów Express (req i res w handlerach pochodzą z express-serve-static-core)
2declare module "express-serve-static-core" {
3 interface Request {
4 user?: {
5 id: string;
6 email: string;
7 roles: string[];
8 };
9 requestId?: string;
10 }
11
12 interface Response {
13 sendSuccess(data: any): void;
14 sendError(error: Error, statusCode?: number): void;
15 }
16}
17
18// Implementacja rozszerzeń
19import express from "express";
20
21const app = express();
22
23// Middleware dodający user i requestId
24app.use((req, res, next) => {
25 req.requestId = crypto.randomUUID();
26 // Autentykacja i dodanie użytkownika
27 req.user = {
28 id: "123",
29 email: "user@example.com",
30 roles: ["user", "admin"]
31 };
32 next();
33});
34
35// Rozszerzenie Response o nowe metody
36app.use((req, res, next) => {
37 res.sendSuccess = function(data: any) {
38 this.json({
39 success: true,
40 data,
41 requestId: req.requestId
42 });
43 };
44
45 res.sendError = function(error: Error, statusCode: number = 500) {
46 this.status(statusCode).json({
47 success: false,
48 error: error.message,
49 requestId: req.requestId
50 });
51 };
52
53 next();
54});
55
56// Używamy rozszerzonych typów
57app.get("/profile", (req, res) => {
58 if (!req.user) {
59 return res.sendError(new Error("Unauthorized"), 401);
60 }
61
62 res.sendSuccess({
63 user: req.user,
64 requestId: req.requestId
65 });
66});Augmentacja zmienia tylko typy, więc implementację sendSuccess i sendError nadal dodaje middleware, zanim użyje ich handler. Zasada ogólna: rozszerzaj moduł, który faktycznie deklaruje typ, a nie ten, który go tylko re-eksportuje.
Rozszerzanie globalnych typów
Blok declare global rozszerza typy globalne, takie jak Window, Array czy String. Działa tylko w pliku, który jest modułem, więc jeśli plik nie ma importów, dodaj na końcu export {}:
1// Rozszerzanie globalnych obiektów
2declare global {
3 interface Window {
4 myApp: {
5 version: string;
6 config: {
7 apiUrl: string;
8 debugMode: boolean;
9 };
10 utils: {
11 formatDate(date: Date): string;
12 parseJSON<T>(json: string): T | null;
13 };
14 };
15 }
16
17 interface Array<T> {
18 groupBy<K extends keyof T>(key: K): Record<string, T[]>;
19 chunk(size: number): T[][];
20 }
21
22 interface String {
23 capitalize(): string;
24 toSlug(): string;
25 }
26}
27
28// Implementacja rozszerzeń Array
29Array.prototype.groupBy = function<T, K extends keyof T>(this: T[], key: K): Record<string, T[]> {
30 return this.reduce((acc, item) => {
31 const groupKey = String(item[key]);
32 if (!acc[groupKey]) {
33 acc[groupKey] = [];
34 }
35 acc[groupKey].push(item);
36 return acc;
37 }, {} as Record<string, T[]>);
38};
39
40Array.prototype.chunk = function<T>(this: T[], size: number): T[][] {
41 const chunks: T[][] = [];
42 for (let i = 0; i < this.length; i += size) {
43 chunks.push(this.slice(i, i + size));
44 }
45 return chunks;
46};
47
48// Implementacja rozszerzeń String
49String.prototype.capitalize = function(this: string): string {
50 return this.charAt(0).toUpperCase() + this.slice(1).toLowerCase();
51};
52
53String.prototype.toSlug = function(this: string): string {
54 return this
55 .toLowerCase()
56 .trim()
57 .replace(/[^\w\s-]/g, '')
58 .replace(/[\s_-]+/g, '-')
59 .replace(/^-+|-+$/g, '');
60};
61
62// Używamy rozszerzeń
63const users = [
64 { name: "Jan", age: 30, department: "IT" },
65 { name: "Anna", age: 25, department: "HR" },
66 { name: "Piotr", age: 35, department: "IT" }
67];
68
69const grouped = users.groupBy("department");
70console.log(grouped); // { IT: [...], HR: [...] }
71
72const chunks = [1, 2, 3, 4, 5, 6, 7, 8].chunk(3);
73console.log(chunks); // [[1, 2, 3], [4, 5, 6], [7, 8]]
74
75const title = "hello world from typescript".capitalize();
76console.log(title); // "Hello world from typescript"
77
78const slug = "Hello World! From TypeScript".toSlug();
79console.log(slug); // "hello-world-from-typescript"
80
81// Window rozszerzenia
82window.myApp = {
83 version: "1.0.0",
84 config: {
85 apiUrl: "https://api.example.com",
86 debugMode: true
87 },
88 utils: {
89 formatDate(date: Date): string {
90 return date.toLocaleDateString("pl-PL");
91 },
92 parseJSON<T>(json: string): T | null {
93 try {
94 return JSON.parse(json) as T;
95 } catch {
96 return null;
97 }
98 }
99 }
100};Technicznie to działa, ale rozszerzania wbudowanych prototypów nie polecam. Właśnie takie łatki (biblioteka Sugar dopisywała tablicom własne groupBy) sprawiły, że standard nie mógł użyć nazwy Array.prototype.groupBy i w ES2024 powstało statyczne Object.groupBy. Bezpieczniej pisać zwykłe funkcje pomocnicze albo rozszerzać własne obiekty, jak window.myApp.
Plugin system z module augmentation
System pluginów pozwala różnym częściom aplikacji dopisywać własne typy kontekstu i hooków do wspólnego modułu:
1// Bazowy system pluginów
2interface PluginSystem {
3 plugins: Map<string, Plugin>;
4 register(plugin: Plugin): void;
5 execute(hookName: string, context: any): void;
6}
7
8interface Plugin {
9 name: string;
10 version: string;
11 hooks?: {
12 [hookName: string]: (context: any) => void | Promise<void>;
13 };
14}
15
16class PluginManager implements PluginSystem {
17 plugins = new Map<string, Plugin>();
18
19 register(plugin: Plugin): void {
20 this.plugins.set(plugin.name, plugin);
21 console.log(`Plugin ${plugin.name} v${plugin.version} zarejestrowany`);
22 }
23
24 async execute(hookName: string, context: any): Promise<void> {
25 for (const [name, plugin] of this.plugins) {
26 if (plugin.hooks && plugin.hooks[hookName]) {
27 await plugin.hooks[hookName](context);
28 }
29 }
30 }
31}
32
33// Rozszerzanie systemu pluginów przez różne moduły
34declare module "./plugin-system" {
35 interface PluginContext {
36 user?: {
37 id: string;
38 name: string;
39 };
40 request?: {
41 url: string;
42 method: string;
43 };
44 }
45
46 interface PluginHooks {
47 beforeRequest?: (context: PluginContext) => void;
48 afterRequest?: (context: PluginContext & { response: any }) => void;
49 onError?: (context: PluginContext & { error: Error }) => void;
50 }
51}
52
53// Plugin autentykacji
54const authPlugin: Plugin = {
55 name: "auth",
56 version: "1.0.0",
57 hooks: {
58 beforeRequest(context) {
59 console.log("Sprawdzanie autentykacji...");
60 if (!context.user) {
61 throw new Error("Brak autentykacji");
62 }
63 }
64 }
65};
66
67// Plugin logowania
68const loggingPlugin: Plugin = {
69 name: "logging",
70 version: "1.0.0",
71 hooks: {
72 beforeRequest(context) {
73 console.log(`[${new Date().toISOString()}] ${context.request?.method} ${context.request?.url}`);
74 },
75 afterRequest(context) {
76 console.log(`Response dla ${context.request?.url}:`, context.response);
77 },
78 onError(context) {
79 console.error(`Błąd dla ${context.request?.url}:`, context.error);
80 }
81 }
82};
83
84// Używanie systemu pluginów
85const pluginManager = new PluginManager();
86pluginManager.register(authPlugin);
87pluginManager.register(loggingPlugin);
88
89// Symulacja żądania
90const context = {
91 user: { id: "123", name: "Jan" },
92 request: { url: "/api/data", method: "GET" }
93};
94
95pluginManager.execute("beforeRequest", context);Augmentacja declare module './plugin-system' działa tylko wtedy, gdy taki plik istnieje, a sam kod musi leżeć w module. W pliku bez importów interfejs Plugin połączyłby się z globalnym interfejsem Plugin z definicji DOM, opisującym wtyczki przeglądarki. To declaration merging w najmniej pożądanej postaci.
Praktyczne zastosowania
Rozszerzanie bibliotek UI
Material UI wprost zachęca do augmentacji: własne pola motywu i warianty typografii dopisujesz do interfejsów Theme, ThemeOptions i TypographyVariants:
1// Rozszerzanie Material-UI
2declare module "@mui/material/styles" {
3 interface Theme {
4 custom: {
5 borderRadius: {
6 small: number;
7 medium: number;
8 large: number;
9 };
10 animations: {
11 fast: string;
12 normal: string;
13 slow: string;
14 };
15 };
16 }
17
18 interface ThemeOptions {
19 custom?: {
20 borderRadius?: {
21 small?: number;
22 medium?: number;
23 large?: number;
24 };
25 animations?: {
26 fast?: string;
27 normal?: string;
28 slow?: string;
29 };
30 };
31 }
32
33 interface TypographyVariants {
34 label: React.CSSProperties;
35 caption2: React.CSSProperties;
36 }
37
38 interface TypographyVariantsOptions {
39 label?: React.CSSProperties;
40 caption2?: React.CSSProperties;
41 }
42}
43
44declare module "@mui/material/Typography" {
45 interface TypographyPropsVariantOverrides {
46 label: true;
47 caption2: true;
48 }
49}
50
51// Tworzenie rozszerzonego theme
52import { createTheme } from "@mui/material/styles";
53
54const theme = createTheme({
55 custom: {
56 borderRadius: {
57 small: 4,
58 medium: 8,
59 large: 16
60 },
61 animations: {
62 fast: "150ms",
63 normal: "300ms",
64 slow: "500ms"
65 }
66 },
67 typography: {
68 label: {
69 fontSize: "0.75rem",
70 fontWeight: 600,
71 textTransform: "uppercase"
72 },
73 caption2: {
74 fontSize: "0.625rem",
75 fontWeight: 400
76 }
77 }
78});
79
80// Używanie rozszerzonych typów
81import { Typography, Box } from "@mui/material";
82
83function CustomComponent() {
84 return (
85 <Box
86 sx={{
87 borderRadius: (theme) => theme.custom.borderRadius.medium,
88 transition: (theme) => `all ${theme.custom.animations.normal} ease`
89 }}
90 >
91 <Typography variant="label">Custom Label</Typography>
92 <Typography variant="caption2">Tiny caption text</Typography>
93 </Box>
94 );
95}Theme opisuje gotowy motyw, a ThemeOptions dane przekazywane do createTheme, dlatego tam pola są opcjonalne. Kod zawiera JSX, więc w projekcie trzymasz go w pliku .tsx.
Rozszerzanie frameworków
Tak samo rozszerzysz typy Next.js dla API routes z Pages Router, dokładając sesję i metody pomocnicze:
1// Rozszerzanie Next.js
2declare module "next" {
3 export interface NextApiRequest {
4 session?: {
5 user: {
6 id: string;
7 email: string;
8 permissions: string[];
9 };
10 expiresAt: Date;
11 };
12
13 // Metody pomocnicze
14 validateBody<T>(schema: any): T;
15 requireAuth(): void;
16 hasPermission(permission: string): boolean;
17 }
18
19 export interface NextApiResponse {
20 success<T>(data: T, statusCode?: number): void;
21 error(message: string, statusCode?: number): void;
22 paginated<T>(items: T[], total: number, page: number, pageSize: number): void;
23 }
24}
25
26// Middleware rozszerzające Next.js
27import { NextApiRequest, NextApiResponse } from "next";
28
29export function withExtensions(
30 handler: (req: NextApiRequest, res: NextApiResponse) => Promise<void>
31) {
32 return async (req: NextApiRequest, res: NextApiResponse) => {
33 // Dodaj metody do request
34 req.validateBody = function<T>(schema: any): T {
35 // Implementacja walidacji
36 return req.body as T;
37 };
38
39 req.requireAuth = function() {
40 if (!this.session) {
41 throw new Error("Authentication required");
42 }
43 };
44
45 req.hasPermission = function(permission: string): boolean {
46 return this.session?.user.permissions.includes(permission) || false;
47 };
48
49 // Dodaj metody do response
50 res.success = function<T>(data: T, statusCode = 200) {
51 this.status(statusCode).json({
52 success: true,
53 data
54 });
55 };
56
57 res.error = function(message: string, statusCode = 500) {
58 this.status(statusCode).json({
59 success: false,
60 error: message
61 });
62 };
63
64 res.paginated = function<T>(items: T[], total: number, page: number, pageSize: number) {
65 this.json({
66 success: true,
67 data: items,
68 pagination: {
69 total,
70 page,
71 pageSize,
72 totalPages: Math.ceil(total / pageSize)
73 }
74 });
75 };
76
77 // Wywołaj handler
78 try {
79 await handler(req, res);
80 } catch (error) {
81 res.error(error instanceof Error ? error.message : "Internal server error");
82 }
83 };
84}
85
86// Użycie w API route
87export default withExtensions(async (req, res) => {
88 req.requireAuth();
89
90 if (!req.hasPermission("read:users")) {
91 return res.error("Insufficient permissions", 403);
92 }
93
94 const data = req.validateBody<{ search?: string }>(/* schema */);
95
96 // Logika biznesowa
97 const users = []; // Pobierz użytkowników
98 const total = 100;
99
100 res.paginated(users, total, 1, 20);
101});W App Router trasy API obsługują Route Handlers oparte na standardowych obiektach Request i Response, więc tam zamiast augmentacji zwykle piszesz funkcje pomocnicze. Przy typach re-eksportowanych sprawdź, z którego pliku pochodzą, tak jak w przypadku Expressa.
Module augmentation i declaration merging to potężne narzędzia do elastycznego rozszerzania istniejącego kodu. Kluczem jest zrozumienie, kiedy używać każdej techniki i jak mogą współpracować w rozszerzalnych systemach. Moja rada: augmentacje trzymaj w osobnym katalogu, na przykład types/, żeby cały zespół wiedział, gdzie szukać dopisków do cudzych typów. W edytorze poniżej przećwiczysz łączenie deklaracji, a w kolejnej lekcji poznasz Test-Driven Development.
Pamiętaj: augmentacja to naklejka z dopiskiem na cudzej mapie parku, która niczego nie wymazuje, tylko dodaje nowe informacje.
Kod do tej lekcji: index.ts
1// Przykłady module augmentation i declaration merging
2
3// 1. Interface merging - łączenie deklaracji
4interface User {
5 id: number;
6 name: string;
7}
8
9interface User {
10 email: string; // Dodajemy nowe właściwości
11 age?: number; // Opcjonalna właściwość
12}
13
14// TypeScript automatycznie łączy obie deklaracje
15const user: User = {
16 id: 1,
17 name: "Jan Kowalski",
18 email: "jan@example.com",
19 age: 30
20};
21
22// 2. Namespace merging
23namespace AppConfig {
24 export interface Settings {
25 apiUrl: string;
26 timeout: number;
27 }
28
29 export function getDefaultSettings(): Settings {
30 return {
31 apiUrl: "https://api.example.com",
32 timeout: 5000
33 };
34 }
35}
36
37namespace AppConfig {
38 export interface Settings {
39 debugMode?: boolean; // Rozszerzamy interfejs
40 }
41
42 export class ConfigManager {
43 private settings: Settings;
44
45 constructor() {
46 this.settings = getDefaultSettings();
47 }
48
49 updateSettings(updates: Partial<Settings>): void {
50 this.settings = { ...this.settings, ...updates };
51 }
52
53 getSettings(): Settings {
54 return this.settings;
55 }
56 }
57}
58
59// Używamy połączonych deklaracji
60const configManager = new AppConfig.ConfigManager();
61configManager.updateSettings({ debugMode: true });
62console.log("Ustawienia:", configManager.getSettings());
63
64// 3. Function i namespace merging
65function createLogger(prefix: string) {
66 return {
67 log(message: string) {
68 console.log(`[${prefix}] ${message}`);
69 }
70 };
71}
72
73namespace createLogger {
74 export interface Options {
75 timestamp?: boolean;
76 color?: string;
77 }
78
79 export function withOptions(prefix: string, options: Options) {
80 return {
81 log(message: string) {
82 const timestamp = options.timestamp ? new Date().toISOString() : '';
83 console.log(`${timestamp} [${prefix}] ${message}`);
84 }
85 };
86 }
87
88 export const DEFAULT_OPTIONS: Options = {
89 timestamp: true,
90 color: 'default'
91 };
92}
93
94// Używamy funkcji i namespace
95const logger1 = createLogger("APP");
96const logger2 = createLogger.withOptions("DEBUG", { timestamp: true });
97
98logger1.log("Prosta wiadomość");
99logger2.log("Wiadomość z timestampem");
100
101// 4. Rozszerzanie globalnych typów
102declare global {
103 interface Array<T> {
104 first(): T | undefined;
105 last(): T | undefined;
106 groupBy<K extends keyof T>(key: K): Record<string, T[]>;
107 }
108
109 interface String {
110 capitalize(): string;
111 toKebabCase(): string;
112 }
113}
114
115// Implementacja rozszerzeń
116Array.prototype.first = function<T>(this: T[]): T | undefined {
117 return this[0];
118};
119
120Array.prototype.last = function<T>(this: T[]): T | undefined {
121 return this[this.length - 1];
122};
123
124Array.prototype.groupBy = function<T, K extends keyof T>(
125 this: T[],
126 key: K
127): Record<string, T[]> {
128 return this.reduce((groups, item) => {
129 const groupKey = String(item[key]);
130 groups[groupKey] = groups[groupKey] || [];
131 groups[groupKey].push(item);
132 return groups;
133 }, {} as Record<string, T[]>);
134};
135
136String.prototype.capitalize = function(this: string): string {
137 return this.charAt(0).toUpperCase() + this.slice(1);
138};
139
140String.prototype.toKebabCase = function(this: string): string {
141 return this
142 .replace(/([A-Z])/g, '-$1')
143 .toLowerCase()
144 .replace(/^-/, '');
145};
146
147// Test rozszerzeń
148const numbers = [1, 2, 3, 4, 5];
149console.log("Pierwszy element:", numbers.first()); // 1
150console.log("Ostatni element:", numbers.last()); // 5
151
152const products = [
153 { name: "Laptop", category: "Electronics", price: 1000 },
154 { name: "Mouse", category: "Electronics", price: 50 },
155 { name: "Desk", category: "Furniture", price: 300 }
156];
157
158const grouped = products.groupBy("category");
159console.log("Pogrupowane produkty:", grouped);
160
161const text = "hello world";
162console.log("Capitalize:", text.capitalize()); // "Hello world"
163
164const camelCase = "myVariableName";
165console.log("Kebab case:", camelCase.toKebabCase()); // "my-variable-name"
166
167// 5. Class i namespace merging
168class EventEmitter {
169 private events: Map<string, Function[]> = new Map();
170
171 on(event: string, handler: Function): void {
172 if (!this.events.has(event)) {
173 this.events.set(event, []);
174 }
175 this.events.get(event)!.push(handler);
176 }
177
178 emit(event: string, ...args: any[]): void {
179 const handlers = this.events.get(event);
180 if (handlers) {
181 handlers.forEach(handler => handler(...args));
182 }
183 }
184}
185
186namespace EventEmitter {
187 export interface EventMap {
188 connect: { userId: string; timestamp: Date };
189 disconnect: { userId: string; reason: string };
190 message: { userId: string; text: string };
191 }
192
193 export type EventHandler<K extends keyof EventMap> =
194 (data: EventMap[K]) => void;
195
196 export function createTypedEmitter() {
197 const emitter = new EventEmitter();
198
199 return {
200 on<K extends keyof EventMap>(event: K, handler: EventHandler<K>): void {
201 emitter.on(event, handler);
202 },
203
204 emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
205 emitter.emit(event, data);
206 }
207 };
208 }
209}
210
211// Używamy typed event emitter
212const typedEmitter = EventEmitter.createTypedEmitter();
213
214typedEmitter.on('connect', (data) => {
215 console.log(`Użytkownik ${data.userId} połączył się o ${data.timestamp}`);
216});
217
218typedEmitter.on('message', (data) => {
219 console.log(`${data.userId}: ${data.text}`);
220});
221
222typedEmitter.emit('connect', {
223 userId: 'user123',
224 timestamp: new Date()
225});
226
227typedEmitter.emit('message', {
228 userId: 'user123',
229 text: 'Witaj wszystkim!'
230});
231
232// 6. Module augmentation (symulacja)
233// W prawdziwej aplikacji byłoby to w osobnym pliku
234interface MyLibrary {
235 version: string;
236 utils: {
237 formatDate(date: Date): string;
238 };
239}
240
241// Rozszerzamy bibliotekę
242interface MyLibrary {
243 plugins: Plugin[];
244 registerPlugin(plugin: Plugin): void;
245}
246
247interface Plugin {
248 name: string;
249 init(): void;
250}
251
252// Implementacja rozszerzonej biblioteki
253const myLibrary: MyLibrary = {
254 version: "1.0.0",
255 utils: {
256 formatDate(date: Date): string {
257 return date.toLocaleDateString('pl-PL');
258 }
259 },
260 plugins: [],
261 registerPlugin(plugin: Plugin): void {
262 this.plugins.push(plugin);
263 plugin.init();
264 console.log(`Plugin ${plugin.name} zarejestrowany`);
265 }
266};
267
268// Test
269const datePlugin: Plugin = {
270 name: "DateFormatter",
271 init() {
272 console.log("DateFormatter plugin zainicjowany");
273 }
274};
275
276myLibrary.registerPlugin(datePlugin);
277console.log("Wersja biblioteki:", myLibrary.version);
278console.log("Sformatowana data:", myLibrary.utils.formatDate(new Date()));
279
280// Export dla uniknięcia błędów globalnych deklaracji
281export {};Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Które deklaracje mogą być automatycznie mergowane w TypeScript?
Zadania praktyczne w grze
- Klikanie w kolejności
Uporządkuj kroki tworzenia ambient modules.
- Edytor kodu
Rozszerz istniejące typy przez declaration merging.
- Klikanie w kolejności
Ułóż elementy deklaracji module augmentation:
- Układanie w pionie
Ułóż kroki rozszerzania modułu przez module augmentation: