Kurs JavaScript i TypeScript · Moduł 10: TypeScript w praktyce
Declaration files (.d.ts) i DefinitelyTyped
W tej lekcji6
Wyobraź sobie, że w Parku Jurajskim odkryto starożytne tablice z opisami gatunków dinozaurów, napisane w nieznanym języku. Naukowcy musieli stworzyć słowniki, żeby zrozumieć, co te tablice opisują. W świecie TypeScript rolę takich słowników pełnią pliki deklaracji (.d.ts): opisują kształt kodu JavaScript, który sam nie ma informacji o typach. Bez nich biblioteka JavaScript jest dla kompilatora typem any (a w trybie strict import kończy się błędem), więc literówkę w nazwie metody odkryjesz dopiero w działającej aplikacji.
Czym są pliki .d.ts?
Pliki deklaracji (declaration files) to pliki z rozszerzeniem .d.ts, które zawierają wyłącznie informacje o typach, bez implementacji. Opisują kształt bibliotek JavaScript, modułów zewnętrznych i globalnych obiektów, a kompilator nie generuje z nich żadnego kodu. Oto opis biblioteki śledzącej dinozaury, w którym declare module 'nazwa' mówi, jak wygląda moduł importowany pod tą nazwą:
1// plik: dinosaur-tracker.d.ts
2// Opisujemy zewnętrzną bibliotekę JavaScript
3
4declare module 'dinosaur-tracker' {
5 export interface DinosaurPosition {
6 id: string;
7 species: string;
8 latitude: number;
9 longitude: number;
10 lastSeen: Date;
11 }
12
13 export function trackDinosaur(id: string): DinosaurPosition;
14 export function getAllPositions(): DinosaurPosition[];
15 export function setAlert(species: string, radius: number): void;
16}Dzięki temu plikowi TypeScript wie, jakie funkcje i typy eksportuje moduł dinosaur-tracker, mimo że sam moduł jest napisany w czystym JavaScript. Implementacja nie zmieniła się ani o bajt, doszedł tylko opis. Gdy sam piszesz bibliotekę w TypeScript, takie pliki wygeneruje kompilator z opcją declaration: true.
Słowo kluczowe declare
Słowo declare informuje TypeScript: ten element istnieje w runtime, ale nie musisz go kompilować, po prostu mi zaufaj. Deklaracja nie tworzy kodu, więc jeśli skłamiesz, kompilator tego nie wykryje. Używamy jej do opisywania zmiennych, funkcji, klas i przestrzeni nazw.
Zmiennych globalnych
Skrypt wczytany tagiem <script> może ustawić globalną konfigurację parku. Opisujemy jej kształt, a nie wartość:
1// Zmienna globalna dostępna w przeglądarce
2declare const PARK_CONFIG: {
3 name: string;
4 maxCapacity: number;
5 securityLevel: 'low' | 'medium' | 'high' | 'critical';
6};
7
8// Teraz możemy jej użyć z pełnym type safety
9console.log(PARK_CONFIG.name);
10console.log(PARK_CONFIG.securityLevel);
11// PARK_CONFIG.unknownField; // Błąd kompilacji!Od teraz odwołanie do nieistniejącego pola to błąd kompilacji, choć sam obiekt tworzy zupełnie inny skrypt.
Funkcji globalnych
Tak samo opisujesz funkcje z zewnętrznego skryptu, podając samą sygnaturę bez ciała:
1// Funkcja zdefiniowana w zewnętrznym skrypcie
2declare function initializeFence(
3 zone: string,
4 voltage: number
5): { active: boolean; zone: string };
6
7declare function emergencyShutdown(): void;
8
9// Użycie z type safety
10const fence = initializeFence('raptor-paddock', 10000);
11console.log(fence.active); // OK
12// console.log(fence.power); // Błąd! Nie ma takiej właściwościDeklaracja mówi, co funkcja przyjmuje i zwraca, a jej kod żyje gdzie indziej.
Klas i namespace
Deklarować można też całe klasy oraz przestrzenie nazw (namespace), które grupują powiązane typy i funkcje, jak w starych bibliotekach ładowanych globalnie:
1declare class SecuritySystem {
2 constructor(zones: string[]);
3 arm(zone: string): void;
4 disarm(zone: string): void;
5 getStatus(): Record<string, boolean>;
6}
7
8declare namespace ParkAPI {
9 interface Visitor {
10 id: string;
11 name: string;
12 ticket: 'standard' | 'vip' | 'researcher';
13 }
14
15 function registerVisitor(name: string, ticket: Visitor['ticket']): Visitor;
16 function getVisitorCount(): number;
17}
18
19// Użycie
20const system = new SecuritySystem(['zone-a', 'zone-b']);
21system.arm('zone-a');
22
23const visitor = ParkAPI.registerVisitor('Alan Grant', 'researcher');
24console.log(visitor.ticket);Wewnątrz declare namespace nie musisz pisać export, bo w kontekście ambient wszystkie składowe są dostępne z zewnątrz.
Pisanie własnych plików deklaracji
Kiedy biblioteka JavaScript nie ma typów, piszesz własny plik .d.ts. Ten opisuje starą bazę dinozaurów. Omit<DinosaurRecord, 'id'> to rekord bez pola id, które nada baza, a Partial<DinosaurRecord> robi wszystkie pola opcjonalnymi:
1// plik: types/legacy-dino-db.d.ts
2// Opisujemy starą bibliotekę JS do zarządzania bazą dinozaurów
3
4declare module 'legacy-dino-db' {
5 export interface DinosaurRecord {
6 id: string;
7 species: string;
8 diet: 'herbivore' | 'carnivore' | 'omnivore';
9 weight: number;
10 height: number;
11 dangerLevel: 1 | 2 | 3 | 4 | 5;
12 }
13
14 export interface QueryOptions {
15 limit?: number;
16 offset?: number;
17 sortBy?: keyof DinosaurRecord;
18 order?: 'asc' | 'desc';
19 }
20
21 export class DinoDB {
22 constructor(connectionString: string);
23 connect(): Promise<void>;
24 disconnect(): Promise<void>;
25 findAll(options?: QueryOptions): Promise<DinosaurRecord[]>;
26 findById(id: string): Promise<DinosaurRecord | null>;
27 insert(record: Omit<DinosaurRecord, 'id'>): Promise<DinosaurRecord>;
28 update(id: string, data: Partial<DinosaurRecord>): Promise<DinosaurRecord>;
29 delete(id: string): Promise<boolean>;
30 }
31
32 // Eksport domyślny
33 export default DinoDB;
34}Typy narzędziowe (utility types) składasz jak klocki: zaczynasz od typu bazowego, wybierasz pola przez Pick lub Omit, modyfikujesz je przez Partial lub Required, a na koniec zabezpieczasz przez Readonly. Dane do aktualizacji opisałby więc typ type UpdateDino = Partial<DinosaurRecord>.
DefinitelyTyped i paczki @types
DefinitelyTyped to ogromne repozytorium na GitHubie z plikami deklaracji dla tysięcy bibliotek JavaScript, rozwijane przez społeczność. Każdy zestaw trafia do npm jako @types/nazwa-biblioteki, więc zamiast pisać własne pliki .d.ts, instalujesz gotowe typy:
1// Instalacja typów dla popularnych bibliotek:
2// npm install --save-dev @types/express
3// npm install --save-dev @types/lodash
4// npm install --save-dev @types/node
5
6// Po instalacji @types/express możesz pisać:
7import express, { Request, Response, NextFunction } from 'express';
8
9const app = express();
10
11// TypeScript zna typy Request, Response, NextFunction
12app.get('/dinosaurs/:id', (req: Request, res: Response) => {
13 const dinoId: string = req.params.id;
14 res.json({ id: dinoId, species: 'T-Rex' });
15});
16
17// Bez @types/express - brak informacji o typach!
18// req, res byłyby typu "any"Paczki @types instalujesz jako zależności deweloperskie (--save-dev), bo typy są potrzebne tylko podczas kompilacji.
Jak TypeScript znajduje pliki deklaracji?
Gdy importujesz moduł, TypeScript szuka typów w kilku miejscach, mniej więcej w tej kolejności:
1// 1. Typy wbudowane w paczkę (pole "types" w package.json)
2// Wiele nowoczesnych bibliotek ma wbudowane .d.ts
3// np. axios, zod, prisma
4
5// 2. Paczki @types z node_modules/@types/
6// Automatycznie rozpoznawane przez TypeScript
7// np. @types/react, @types/node
8
9// 3. Własne pliki .d.ts w projekcie
10// Konfiguracja w tsconfig.json:
11// {
12// "compilerOptions": {
13// "typeRoots": ["./node_modules/@types", "./types"],
14// "types": ["node", "express"]
15// },
16// "include": ["src/**/*", "types/**/*"]
17// }
18
19// 4. Triple-slash directives (rzadko używane)
20/// <reference types="node" />
21/// <reference path="./custom-types.d.ts" />Zanim zainstalujesz @types, sprawdź, czy biblioteka nie ma własnych typów. Coraz więcej paczek, na przykład axios czy zod, dostarcza je w środku i wtedy osobna paczka jest zbędna.
Ambient module declarations
Czasami potrzebujesz zadeklarować moduł, który nie jest pakietem npm, na przykład plik CSS, obraz lub JSON. Gwiazdka we wzorcu '*.css' pasuje do każdej ścieżki z tym rozszerzeniem:
1// plik: types/assets.d.ts
2
3// Import plików CSS
4declare module '*.css' {
5 const classes: Record<string, string>;
6 export default classes;
7}
8
9// Import plików graficznych
10declare module '*.png' {
11 const src: string;
12 export default src;
13}
14
15declare module '*.svg' {
16 const content: string;
17 export default content;
18}
19
20// Import plików JSON
21declare module '*.json' {
22 const value: Record<string, unknown>;
23 export default value;
24}
25
26// Teraz możesz importować te pliki z type safety:
27// import styles from './styles.css';
28// import logo from './logo.png';
29// import dinoData from './dinosaurs.json';Deklaracja mówi tylko, co zwróci import, a samo wczytanie pliku zapewnia bundler. Dla JSON-a możesz zamiast niej włączyć opcję resolveJsonModule, która podaje dokładny typ zawartości.
Rozszerzanie istniejących typów (Declaration Merging w .d.ts)
Pliki .d.ts pozwalają też rozszerzać typy z zewnętrznych bibliotek. Dodajemy do żądania Express pola, które ustawia nasz middleware:
1// plik: types/express-extension.d.ts
2// Rozszerzamy typy Express o dodatkowe pola
3
4import 'express';
5
6// Typ req w handlerach pochodzi z paczki express-serve-static-core
7declare module 'express-serve-static-core' {
8 interface Request {
9 userId?: string;
10 parkZone?: string;
11 securityClearance?: 'visitor' | 'staff' | 'admin';
12 }
13}
14
15// Teraz w kodzie aplikacji:
16// app.use((req, res, next) => {
17// req.userId = 'USR-001'; // OK - TypeScript zna to pole
18// req.parkZone = 'zone-a'; // OK
19// req.securityClearance = 'staff'; // OK
20// next();
21// });Interfejs Request łączy się z oryginałem (declaration merging), zamiast go zastępować. Rozszerzamy moduł express-serve-static-core, bo stamtąd pochodzi typ req w handlerach, a import 'express' czyni plik modułem, co jest warunkiem rozszerzenia modułu.
Pliki deklaracji (.d.ts) i ekosystem DefinitelyTyped to fundamenty pracy z TypeScript w realnym świecie: dzięki nim korzystasz z tysięcy bibliotek JavaScript z pełnym type safety. Moja kolejność: najpierw typy wbudowane w paczkę, potem @types, a własny plik .d.ts na końcu, zaczynając od funkcji, których naprawdę używasz. W edytorze poniżej przećwiczysz deklaracje, a w kolejnej lekcji poznasz template literal types.
Pamiętaj: plik .d.ts to słownik do starożytnej tablicy, który nie zmienia na niej ani znaku, tylko pozwala ją bezpiecznie odczytać.
Kod do tej lekcji: index.ts
1// Declaration files (.d.ts) i DefinitelyTyped
2// Park Jurajski - System Deklaracji Typów
3
4// 1. Symulacja: deklarowanie zewnętrznej biblioteki JS
5// W prawdziwym projekcie to byłoby w pliku .d.ts
6
7// declare module 'dinosaur-tracker' {
8// export function trackDinosaur(id: string): DinosaurPosition;
9// }
10
11// 2. Deklarowanie zmiennych globalnych
12// declare const PARK_CONFIG: { name: string; securityLevel: string };
13
14// 3. Symulacja użycia typów z DefinitelyTyped
15// Wyobraźmy sobie, że zainstalowaliśmy @types/express
16
17interface DinosaurRecord {
18 id: string;
19 species: string;
20 diet: 'herbivore' | 'carnivore' | 'omnivore';
21 weight: number;
22 dangerLevel: 1 | 2 | 3 | 4 | 5;
23}
24
25interface QueryOptions {
26 limit?: number;
27 offset?: number;
28 sortBy?: keyof DinosaurRecord;
29 order?: 'asc' | 'desc';
30}
31
32// Symulacja klasy z zewnętrznej biblioteki
33class DinoDB {
34 private records: DinosaurRecord[] = [
35 { id: 'D001', species: 'T-Rex', diet: 'carnivore', weight: 7000, dangerLevel: 5 },
36 { id: 'D002', species: 'Triceratops', diet: 'herbivore', weight: 6000, dangerLevel: 2 },
37 { id: 'D003', species: 'Velociraptor', diet: 'carnivore', weight: 80, dangerLevel: 4 },
38 { id: 'D004', species: 'Brachiosaurus', diet: 'herbivore', weight: 56000, dangerLevel: 1 },
39 ];
40
41 findAll(options?: QueryOptions): DinosaurRecord[] {
42 let result = [...this.records];
43 if (options?.sortBy) {
44 result.sort((a, b) => {
45 const val = String(a[options.sortBy!]).localeCompare(String(b[options.sortBy!]));
46 return options.order === 'desc' ? -val : val;
47 });
48 }
49 const offset = options?.offset || 0;
50 const limit = options?.limit || result.length;
51 return result.slice(offset, offset + limit);
52 }
53
54 findById(id: string): DinosaurRecord | null {
55 return this.records.find(r => r.id === id) || null;
56 }
57}
58
59// 4. Rozszerzanie typów (Declaration Merging)
60// W prawdziwym projekcie rozszerzamy np. Express.Request
61
62interface ParkRequest {
63 userId?: string;
64 parkZone?: string;
65 securityClearance?: 'visitor' | 'staff' | 'admin';
66}
67
68// Test
69const db = new DinoDB();
70
71console.log("=== Baza dinozaurów (symulacja @types) ===");
72console.log("\nWszystkie rekordy:");
73db.findAll().forEach(d =>
74 console.log(` ${d.species} (diet: ${d.diet}, danger: ${d.dangerLevel})`)
75);
76
77console.log("\nPosortowane po wadze (desc), limit 2:");
78db.findAll({ sortBy: 'weight', order: 'desc', limit: 2 }).forEach(d =>
79 console.log(` ${d.species}: ${d.weight}kg`)
80);
81
82console.log("\nSzukam T-Rex:");
83const trex = db.findById('D001');
84if (trex) {
85 console.log(` Znaleziono: ${trex.species}, danger level: ${trex.dangerLevel}`);
86}
87
88// 5. Ambient declarations - typy dla zasobów
89// declare module '*.css' { const classes: Record<string, string>; export default classes; }
90// declare module '*.png' { const src: string; export default src; }
91
92console.log("\n=== Typy deklaracji (.d.ts) ===");
93console.log("• declare module - deklaracja modułu zewnętrznego");
94console.log("• declare const/function - globalne zmienne/funkcje");
95console.log("• @types/xxx - gotowe typy z DefinitelyTyped");
96console.log("• *.d.ts - pliki z samymi typami, bez implementacji");Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co zawierają pliki z rozszerzeniem .d.ts w TypeScript?
2. Czym jest repozytorium DefinitelyTyped i paczki @types?
Zadania praktyczne w grze
- Edytor kodu
Napisz deklaracje typów dla zewnętrznej biblioteki i zmiennych globalnych używając declare.
- Klikanie w kolejności
Ułóż elementy deklaracji modułu zewnętrznego w TypeScript:
- Edytor kodu
Stwórz deklaracje typów dla modułu dinosaur-tracker używając declare module.
- Układanie w poziomie
Ułóż elementy deklaracji typu Partial w odpowiedniej kolejności:
- Układanie w pionie
Ułóż kroki budowania złożonego typu narzędziowego: