Przejdź do treści

Temat 36 · 2 × 45 min

Co dalej: TypeScript i Node.js

Znasz już cały JavaScript, którego potrzebuje przeglądarka. Ta lekcja pokazuje dwa kierunki, w które ten sam język rozrósł się poza nią. TypeScript dodaje do JS typy sprawdzane przed uruchomieniem – dzięki temu edytor podkreśla błąd cena.toFixd(2) w chwili pisania, a nie użytkownik po kliknięciu. Node.js uruchamia JavaScript bez przeglądarki: na twoim komputerze, na serwerze, w narzędziach takich jak Vite czy Vitest, których używałeś w lekcjach 32 i 33. Oba tematy są „zarysem”: po tej lekcji rozumiesz kod, który spotkasz w tutorialach i projektach, i wiesz, od czego zacząć samodzielnie.

Część 1. TypeScript

Czym jest TypeScript

TypeScript (Microsoft, 2012) to JavaScript plus adnotacje typów. Każdy poprawny program JS jest poprawnym programem TS; TS dodaje składnię do opisywania, jakiego typu są wartości, i kompilator tsc, który sprawdza, czy ten opis się zgadza. Typy istnieją tylko w czasie pisania: kompilator usuwa je i produkuje zwykły JavaScript, który uruchamia przeglądarka lub Node. W czasie działania nie ma żadnego sprawdzania – TS łapie błędy wcześniej, nie dodatkowo.

raty.ts
function obliczRate(koszt: number, liczbaRat: number): number {
  return koszt / liczbaRat;
}

obliczRate(1800, 3);      // OK
obliczRate('1800', 3);    // błąd kompilacji: Argument of type 'string' is not assignable to parameter of type 'number'
obliczRate(1800);         // błąd: Expected 2 arguments, but got 1

Po kompilacji zostaje to, co znasz:

raty.js (po kompilacji)
function obliczRate(koszt, liczbaRat) {
  return koszt / liczbaRat;
}
console.log(obliczRate(1800, 3));   // 600
console.log(obliczRate('1800', 3)); // 600 – JS sam skonwertował; TS nie pozwoliłby tego napisać
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Zauważ, że drugie wywołanie w JavaScripcie działa (konwersja napisu na liczbę przy dzieleniu). Z '1800' + 3 byłby już napis '18003' – klasyczny błąd z lekcji 3, który TypeScript wychwyci, zanim uruchomisz kod.

Instalacja i pierwsza kompilacja

Terminal
mkdir ts-start && cd ts-start
npm init -y
npm install -D typescript        # TypeScript 7.x (2026) – kompilator napisany w Go, ok. 10× szybszy od 5.x
npx tsc --init                   # tworzy tsconfig.json
npx tsc                          # kompiluje wszystkie .ts w projekcie do .js
npx tsc --watch                  # kompiluje po każdym zapisie

tsconfig.json to konfiguracja kompilatora. Najważniejsze pola: target (do której wersji JS tłumaczyć – dla współczesnych przeglądarek es2022 lub esnext), strict (włącza wszystkie ścisłe sprawdzenia – od TypeScript 6 domyślnie włączony, w starszych wersjach ustaw ręcznie true), outDir (folder na wynikowe .js). Do szybkich prób nie potrzebujesz nawet instalacji: TypeScript Playground kompiluje w przeglądarce i pokazuje JS obok TS. W projekcie Vite wystarczy szablon react-ts lub vanilla-ts – kompilację robi Vite.

Typy podstawowe i wnioskowanie

Adnotacja typu to dwukropek po nazwie: let cena: number. Ale TypeScript potrafi wywnioskować typ z wartości początkowej – adnotacje piszesz głównie tam, gdzie wnioskowanie nie ma z czego skorzystać: parametry funkcji, puste tablice, zmienne bez wartości.

typy.ts
let imie = 'Ola';            // wywnioskowane: string
let wiek: number = 17;       // adnotacja (tu zbędna – 17 i tak jest number)
let aktywny = true;          // boolean
let oceny: number[] = [];    // pusta tablica – trzeba powiedzieć, co w niej będzie
let nic: null = null;

imie = 42;                   // błąd: Type 'number' is not assignable to type 'string'
oceny.push('5');             // błąd: '5' to string
Typ TSCo oznaczaPrzykład
string, number, booleantypy proste jak w JS (małe litery!)let x: number = 5
number[] lub Array<number>tablica liczb[1, 2, 3]
[string, number]krotka – tablica o stałej długości i typach['Ola', 17]
null, undefinedbrak wartości; w strict nie wchodzą do innych typów`let a: string
any„wyłącz sprawdzanie” – unikajwszystko przechodzi
unknown„nie wiem, co to” – trzeba sprawdzić przed użyciemwynik JSON.parse
voidfunkcja nic nie zwraca(): void => {}
`‘tak’‘nie’`typ literalny – tylko te wartości
Sprawdź się

Która linia spowoduje błąd kompilacji TypeScript?

let liczba = 10;
let napis = 'dziesięć';
liczba = liczba + 5;        // A
napis = napis + liczba;     // B
liczba = napis.length;      // C
liczba = napis;             // D

Unie i zawężanie

Unia (union) A | B mówi: wartość jest typu A lub B. TypeScript nie pozwoli wywołać metody, której nie mają oba typy – najpierw musisz zawęzić typ sprawdzeniem (typeof, ===, in, Array.isArray), a kompilator śledzi te sprawdzenia w kodzie.

unie.ts
function formatujId(id: number | string): string {
  // tu id to number | string – toUpperCase niedostępne
  if (typeof id === 'string') {
    return id.toUpperCase();      // tu TS wie: string
  }
  return id.toFixed(0);           // tu TS wie: number
}

type Status = 'nowe' | 'w toku' | 'zrobione';   // unia literałów = wyliczenie bez enum

function opisz(status: Status): string {
  switch (status) {
    case 'nowe': return 'Do zrobienia';
    case 'w toku': return 'Pracujemy';
    case 'zrobione': return 'Gotowe';
  }
}

opisz('zrobione');   // OK
opisz('gotowe');     // błąd: Argument of type '"gotowe"' is not assignable to parameter of type 'Status'

Literówka w nazwie statusu – w JavaScripcie cichy błąd, który zauważysz dopiero, gdy zadanie nie pojawi się na liście. W TypeScripcie czerwone podkreślenie w edytorze.

Obiekty: type i interface

Kształt obiektu opisujesz aliasem typu (type) albo interfejsem (interface). W codziennym użyciu są wymienne; interface można rozszerzać przez extends i jest częściej używany dla obiektów, type – dla unii i typów złożonych. Znak ? oznacza pole opcjonalne, readonly – tylko do odczytu.

obiekty.ts
interface Uczen {
  readonly id: number;
  imie: string;
  klasa: string;
  oceny: number[];
  opiekun?: string;          // opcjonalne – może nie istnieć
}

type Produkt = {
  nazwa: string;
  cena: number;
  dostepny: boolean;
};

const ola: Uczen = { id: 1, imie: 'Ola', klasa: '3TP', oceny: [5, 4] };
ola.id = 2;                 // błąd: readonly
ola.klasa = 4;              // błąd: number zamiast string
const bez: Uczen = { id: 2, imie: 'Jan' };   // błąd: brakuje klasa i oceny

function srednia(u: Uczen): number {
  if (u.oceny.length === 0) return 0;
  return u.oceny.reduce((s, o) => s + o, 0) / u.oceny.length;
}

// interfejs rozszerzony
interface Absolwent extends Uczen {
  rokMatury: number;
}

Dane z fetch i JSON.parse mają typ any (TS nie zna odpowiedzi serwera), więc ty mówisz, czego się spodziewasz: const dane = (await odp.json()) as Uczen[]. Słowo as to asercja – obietnica wobec kompilatora, nie sprawdzenie w czasie działania. Jeśli serwer przyśle coś innego, błąd pojawi się jak w zwykłym JS.

Funkcje

funkcje.ts
// parametry z typami, typ zwracany po nawiasie
function rabat(cena: number, procent: number = 10): number {
  return cena * (1 - procent / 100);
}

// parametr opcjonalny (?) musi być na końcu
function powitaj(imie: string, tytul?: string): string {
  return tytul ? `${tytul} ${imie}` : imie;
}

// funkcja strzałkowa z typem – np. callback dla addEventListener
const naKlik = (e: MouseEvent): void => {
  console.log(e.clientX);
};

// typ funkcji jako parametr
function zastosuj(liczby: number[], fn: (x: number) => number): number[] {
  return liczby.map(fn);
}
zastosuj([1, 2, 3], (x) => x * 2);   // x wywnioskowane jako number

TypeScript odróżnia też funkcję, która zwraca number, od tej, która może zwrócić number | undefined (np. tablica.find(...)). W trybie strict zanim użyjesz wyniku find, musisz sprawdzić, czy nie jest undefined – to dokładnie ten błąd „Cannot read properties of undefined”, który w JS widziałeś w konsoli po fakcie.

Typowanie DOM

API DOM jest w całości opisane typami. document.getElementById zwraca HTMLElement | null – bo element może nie istnieć – a ogólny HTMLElement nie ma pola value. Zawężasz asercją do konkretnego typu i sprawdzasz null:

dom.ts
const pole = document.getElementById('cena') as HTMLInputElement | null;
const wynik = document.querySelector<HTMLParagraphElement>('#wynik');

if (pole && wynik) {
  pole.addEventListener('input', () => {
    const cena = parseFloat(pole.value);          // value: string – jak w JS
    wynik.textContent = `Brutto: ${(cena * 1.23).toFixed(2)} zł`;
  });
}

Irytujące? Trochę. Ale każde z tych sprawdzeń odpowiada realnemu błędowi egzaminacyjnemu z lekcji 19: literówka w id i skrypt, który nie startuje.

Sprawdź się

Dlaczego TypeScript w trybie strict odrzuci tę linię?

const przycisk = document.getElementById('oblicz');
przycisk.addEventListener('click', licz);

Generyki – pierwszy kontakt

Czasem funkcja działa tak samo dla dowolnego typu: „zwróć pierwszy element tablicy”. Zamiast pisać ją trzy razy albo używać any, wprowadzasz parametr typu <T> – zmienną, w którą TypeScript podstawi konkretny typ przy wywołaniu.

generyki.ts
function pierwszy<T>(tablica: T[]): T | undefined {
  return tablica[0];
}

const a = pierwszy([1, 2, 3]);           // a: number | undefined
const b = pierwszy(['x', 'y']);          // b: string | undefined
const c = pierwszy<Uczen>([]);           // c: Uczen | undefined

Generyki widzisz w typach wbudowanych: Array<number>, Promise<Uczen[]> (obietnica tablicy uczniów – tak opiszesz wynik async funkcji z fetch), Map<string, number>. Na tym etapie wystarczy, że je czytasz; pisać własne będziesz, gdy zauważysz, że kopiujesz funkcję dla różnych typów.

Tryb strict i any

strict to pakiet reguł: noImplicitAny (każdy parametr bez typu to błąd, nie any), strictNullChecks (null/undefined trzeba obsłużyć), i kilka innych. Nowe projekty zawsze zaczynaj ze strict – od TypeScript 6 jest domyślny. Pokusa „wpiszę any, bo się czerwieni” jest duża; any wyłącza TypeScript dla tej wartości i wszystkiego, co z niej wynika. Jeśli naprawdę nie wiesz, czym coś jest (dane z sieci), użyj unknown i sprawdź.

Część 2. Node.js

Czym jest Node.js

Node.js (2009) to silnik V8 z Chrome wyjęty z przeglądarki i uzupełniony o to, czego przeglądarka nie daje: dostęp do plików, sieci, procesów. Ten sam język, te same const, Promise, async/await, fetch, JSON – ale bez document, window, alert i DOM, bo nie ma strony. Zamiast nich moduły wbudowane: node:fs (pliki), node:http (serwer), node:path, process (argumenty, zmienne środowiskowe).

Wersje (październik 2026): Node 24 „Krypton” to aktualne LTS – wersja do projektów; Node 26 jest w fazie Current i stanie się LTS w październiku 2026; Node 22 jest jeszcze wspierane. Sprawdź swoją: node --version. Node masz zainstalowany od lekcji 32 – bez niego nie działałby ani npm, ani Vite.

Pierwszy skrypt i process.argv

powitanie.js
const imie = process.argv[2] ?? 'nieznajomy';   // [0] = node, [1] = ścieżka skryptu, [2] = pierwszy argument
console.log(`Cześć, ${imie}! Node ${process.version}`);
Terminal
node powitanie.js Ola
# Cześć, Ola! Node v24.11.0
node powitanie.js
# Cześć, nieznajomy! Node v24.11.0

process.argv to tablica napisów z wiersza poleceń – argumenty liczbowe konwertujesz sam (Number(process.argv[3])), tak jak input.value w przeglądarce. console.log pisze do terminala. Ten mechanizm możesz zasymulować w zwykłym bloku:

argv-symulacja.js
// tak wygląda process.argv dla: node raport.js oceny.json 3
const argv = ['/usr/bin/node', '/home/ola/raport.js', 'oceny.json', '3'];
const [plik, prog] = argv.slice(2);          // pomijasz node i ścieżkę skryptu
console.log('plik:', plik, '| próg:', Number(prog));
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Moduły ES w Node

Node obsługuje dwa systemy modułów: starszy CommonJS (require, module.exports – zobaczysz go w wielu tutorialach) i standardowe moduły ES (import/export) z lekcji 32. Żeby pliki .js były traktowane jako moduły ES, wpisz w package.json "type": "module" (albo nadaj plikom rozszerzenie .mjs). Moduły wbudowane importujesz z przedrostkiem node:.

package.json
{
  "name": "raport-ocen",
  "type": "module",
  "scripts": {
    "start": "node raport.js oceny.json",
    "dev": "node --watch raport.js oceny.json",
    "api": "json-server db.json"
  }
}
narzedzia.js
export function srednia(liczby) {
  if (liczby.length === 0) return 0;
  return liczby.reduce((s, x) => s + x, 0) / liczby.length;
}
raport.js
import { readFile } from 'node:fs/promises';
import { srednia } from './narzedzia.js';      // w Node rozszerzenie .js jest obowiązkowe

const sciezka = process.argv[2] ?? 'oceny.json';
const tekst = await readFile(sciezka, 'utf8');  // top-level await działa w modułach ES
const uczniowie = JSON.parse(tekst);

for (const u of uczniowie) {
  console.log(`${u.imie.padEnd(10)} ${srednia(u.oceny).toFixed(2)}`);
}
oceny.json
[
  { "imie": "Ola", "oceny": [5, 4, 6] },
  { "imie": "Bartek", "oceny": [3, 3, 4, 2] },
  { "imie": "Kasia", "oceny": [] }
]
Terminal
npm start
# Ola        5.00
# Bartek     3.00
# Kasia      0.00

Trzy rzeczy do zapamiętania: readFile z node:fs/promises zwraca Promise (stąd await), drugi argument 'utf8' daje napis zamiast surowych bajtów, a wczytany JSON parsujesz tak samo jak odpowiedź z fetch. Zapis jest symetryczny: await writeFile('wynik.json', JSON.stringify(dane, null, 2)).

Sprawdź się

Co wypisze ten skrypt uruchomiony poleceniem node suma.js 4 5?

const [a, b] = process.argv.slice(2);
console.log(a + b);

Skrypty npm i tryb —watch

Sekcja scripts w package.json to nazwane polecenia projektu: npm run dev, npm run api (start i test działają bez run). Dzięki niej każdy w zespole uruchamia projekt tak samo, a w skryptach możesz używać paczek z node_modules bez npx. Flaga node --watch plik.js restartuje skrypt po każdej zmianie pliku – odpowiednik odświeżania Vite.

Mini serwer HTTP

Serwer w Node to funkcja wywoływana dla każdego żądania: dostaje req (co przyszło: adres, metoda) i res (co odsyłasz: status, nagłówki, treść). Poniżej serwer, który pod /api/zadania zwraca JSON, a wszędzie indziej 404 – wystarczający, żeby frontend z lekcji 31 miał z czym rozmawiać.

serwer.js
import { createServer } from 'node:http';

const zadania = [
  { id: 1, tekst: 'Powtórzyć pętle', zrobione: true },
  { id: 2, tekst: 'Zrobić arkusz 01.2025', zrobione: false },
];

const serwer = createServer((req, res) => {
  console.log(req.method, req.url);           // log każdego żądania w terminalu
  res.setHeader('Access-Control-Allow-Origin', '*');   // CORS – pozwól stronie z innego portu

  if (req.method === 'GET' && req.url === '/api/zadania') {
    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify(zadania));
    return;
  }
  res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('Nie znaleziono');
});

serwer.listen(3000, () => console.log('Serwer działa: http://localhost:3000/api/zadania'));

Po node serwer.js wejdź w przeglądarce na http://localhost:3000/api/zadania – zobaczysz JSON. Z frontu pobierzesz go zwykłym fetch('http://localhost:3000/api/zadania'). Obsługa POST (czytanie treści żądania), wielu tras i bazy danych to już zadanie dla frameworka (Express, Fastify, Hono) – w INF.03 tę rolę pełni PHP.

json-server: REST w jedną minutę

Do ćwiczenia frontendu nie piszesz własnego serwera. json-server zamienia plik JSON w pełne REST API z GET/POST/PUT/PATCH/DELETE:

Terminal
npm install -D json-server
npx json-server db.json
# JSON Server started on PORT :3000
# http://localhost:3000/zadania
db.json
{
  "zadania": [
    { "id": "1", "tekst": "Powtórzyć pętle", "zrobione": true },
    { "id": "2", "tekst": "Zrobić arkusz 01.2025", "zrobione": false }
  ]
}

Każda tablica w db.json staje się zasobem: GET /zadania, GET /zadania/1, POST /zadania (z JSON w treści), DELETE /zadania/2. Zmiany zapisują się do pliku. Aktualna wersja 1.x (beta) nadaje identyfikatorom typ napisu – stąd "id": "1". To ten sam json-server, którego używałeś w lekcji 31; teraz wiesz, że to zwykły skrypt Node.

Node uruchamia TypeScript

Od Node 22.18 i 23.6 (a stabilnie od 24.12) polecenie node skrypt.ts działa bez instalowania czegokolwiek: Node usuwa typy (type stripping) i wykonuje kod. Ważne ograniczenia:

  • Node nie sprawdza typów – literówka w typie przejdzie; do sprawdzania nadal potrzebujesz tsc (lub edytora, który uruchamia go w tle),
  • działa tylko składnia „wymazywalna”: adnotacje, interface, type; enum, namespace z kodem i parametry-właściwości w konstruktorze kończą się błędem ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX (w Node 22–25 dało się je włączyć eksperymentalną flagą --experimental-transform-types, ale Node 26 tę flagę usunął – zamiast enum używaj unii literałów),
  • w importach podajesz pełne rozszerzenie: import { f } from './plik.ts',
  • typy importujesz ze słowem type: import type { Uczen } from './typy.ts' – inaczej Node szuka nieistniejącej wartości.
raport.ts
import { readFile } from 'node:fs/promises';

interface Uczen {
  imie: string;
  oceny: number[];
}

const uczniowie = JSON.parse(await readFile('oceny.json', 'utf8')) as Uczen[];
for (const u of uczniowie) {
  const s = u.oceny.length ? u.oceny.reduce((a, b) => a + b, 0) / u.oceny.length : 0;
  console.log(`${u.imie}: ${s.toFixed(2)}`);
}
Terminal
node raport.ts        # Node 24+: działa od razu
npx tsc --noEmit      # a tu sprawdzasz typy, bez generowania .js

Gdy potrzebujesz pełnej składni TS (np. enum), popularne jest narzędzie tsx (npx tsx raport.ts) – ono jednak też nie sprawdza typów, tylko je przekształca; sprawdzanie zawsze robi tsc. W projektach Vite nic z tego nie konfigurujesz – robi to szablon.

Gdzie to wszystko się spotyka

MiejsceCo uruchamia JSTypowe narzędzia
przeglądarka użytkownikatwój front (lekcje 1–35)Vite buduje, React renderuje
twój komputer – narzędziaNodenpm, Vite, Vitest, ESLint, tsc
serwer aplikacjiNode (INF.04) lub PHP/Python (INF.03)Express/Fastify; json-server do ćwiczeń
wszystko powyżejTypeScript → JStsc, szablony -ts w Vite

Pułapki – podsumowanie

BłądObjawPoprawka
any „żeby przestało się czerwienić”TS nic nie sprawdza dalejwłaściwy typ albo unknown + sprawdzenie
brak sprawdzenia null po getElementByIdbłąd strict / TypeError w JSif (el) lub el?.
String, Number wielką literą w typachdziała, ale to typy obiektów opakowującychzawsze string, number
mieszanie require i import w jednym projekcieReferenceError: require is not defined w module ES"type": "module" i tylko import
import './plik' bez rozszerzenia w NodeERR_MODULE_NOT_FOUND./plik.js / ./plik.ts
process.argv[2] + 1sklejony napisNumber(...)
readFileSync w serwerzeblokuje wszystkie żądaniafs/promises + await
enum w pliku uruchamianym przez node plik.tsERR_UNSUPPORTED_TYPESCRIPT_SYNTAXunia literałów albo tsx (flaga --experimental-transform-types działała tylko do Node 25)
__dirname w module ESundefinedimport.meta.dirname (Node 20.11+)

Ściąga

SkładniaCo robiPrzykład
let x: numberadnotacja typulet cena: number = 9.99
number[], Array<T>tablicaconst oceny: number[] = []
`AB`unia
`‘a’‘b’`typ literalny
interface N { pole: T; opc?: T }kształt obiektuinterface Uczen { imie: string }
type N = {…}alias typutype Punkt = { x: number; y: number }
function f(a: T): Rtypy funkcji(cena: number): string
wartosc as Typasercja (bez sprawdzania!)el as HTMLInputElement
function f<T>(x: T): Tgenerykpierwszy<Uczen>(lista)
npx tsc / --watch / --noEmitkompilacja / na bieżąco / tylko sprawdzenienpx tsc --noEmit
node plik.js / node plik.tsuruchomienie skryptu.ts od Node 22.18/23.6
process.argv.slice(2)argumenty wiersza poleceń (napisy)node s.js a b → ['a','b']
import { readFile } from 'node:fs/promises'plikiawait readFile('x.json', 'utf8')
"type": "module"moduły ES w Nodew package.json
"scripts": { "dev": "…" }skrypty npmnpm run dev
createServer((req, res) => …).listen(3000)serwer HTTPnode:http
npx json-server db.jsonREST API z plikuGET /zadania/1

Zadania

Zadanie 1
łatwe

Otypuj kalkulator

Poniższe funkcje z lekcji 14 są w czystym JS. Przepisz je do TypeScriptu: dodaj typy parametrów i wartości zwracanych, a koszt z undefined zamień na parametr opcjonalny z wartością domyślną. Sprawdź w TypeScript Playground, że nie ma błędów, a wywołanie obliczRate('1800', 3) jest odrzucane.

kalkulator.js
function obliczKoszt(ceny, wybrane) {
  let suma = 0;
  for (const nazwa of wybrane) suma += ceny[nazwa];
  return suma;
}
function obliczRate(koszt, liczbaRat) {
  return Math.round((koszt / liczbaRat) * 100) / 100;
}
function komunikat(miasto, koszt, liczbaRat, rata) {
  return `Kurs odbędzie się w ${miasto}. Koszt całkowity: ${koszt} zł. Płacisz ${liczbaRat} rat po ${rata} zł`;
}
Pokaż rozwiązanie
kalkulator.ts
// Cennik: klucz to nazwa kursu, wartość to cena – typ indeksowy
type Cennik = Record<string, number>;   // to samo co { [nazwa: string]: number }

function obliczKoszt(ceny: Cennik, wybrane: string[]): number {
  let suma = 0;
  for (const nazwa of wybrane) suma += ceny[nazwa] ?? 0;   // ?? 0 – nieznany kurs nie daje NaN
  return suma;
}

function obliczRate(koszt: number, liczbaRat: number = 1): number {
  return Math.round((koszt / liczbaRat) * 100) / 100;
}

function komunikat(miasto: string, koszt: number, liczbaRat: number, rata: number): string {
  return `Kurs odbędzie się w ${miasto}. Koszt całkowity: ${koszt} zł. Płacisz ${liczbaRat} rat po ${rata} zł`;
}

const ceny: Cennik = { HTML: 500, CSS: 400, JavaScript: 900 };
const koszt = obliczKoszt(ceny, ['HTML', 'JavaScript']);
console.log(komunikat('Poznań', koszt, 3, obliczRate(koszt, 3)));

// obliczRate('1800', 3);  // błąd: Argument of type 'string' is not assignable to parameter of type 'number'

Po usunięciu typów to dokładnie ten sam kod, co w kalkulator.js – możesz to sprawdzić w prawej kolumnie Playgroundu. Wynik działania:

kalkulator.js (po kompilacji)
function obliczKoszt(ceny, wybrane) {
  let suma = 0;
  for (const nazwa of wybrane) suma += ceny[nazwa] ?? 0;
  return suma;
}
function obliczRate(koszt, liczbaRat = 1) {
  return Math.round((koszt / liczbaRat) * 100) / 100;
}
function komunikat(miasto, koszt, liczbaRat, rata) {
  return `Kurs odbędzie się w ${miasto}. Koszt całkowity: ${koszt} zł. Płacisz ${liczbaRat} rat po ${rata} zł`;
}
const ceny = { HTML: 500, CSS: 400, JavaScript: 900 };
const koszt = obliczKoszt(ceny, ['HTML', 'JavaScript']);
console.log(komunikat('Poznań', koszt, 3, obliczRate(koszt, 3)));
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Zadanie 2
średnie

Interfejs Zadanie i filtrowanie po statusie

Zdefiniuj typ Status jako unię literałów 'nowe' | 'w toku' | 'zrobione' oraz interfejs Zadanie z polami: id (liczba, readonly), tytul (napis), status (Status), termin (napis, opcjonalny). Napisz funkcje:

  • wgStatusu(zadania: Zadanie[], status: Status): Zadanie[],
  • zmienStatus(zadanie: Zadanie, nowy: Status): Zadanie – zwraca nowy obiekt (bez mutacji, jak w React),
  • podsumowanie(zadania: Zadanie[]): Record<Status, number> – ile zadań ma każdy status.

Sprawdź, że zmienStatus(z, 'gotowe') nie kompiluje się.

Pokaż rozwiązanie
zadania.ts
type Status = 'nowe' | 'w toku' | 'zrobione';

interface Zadanie {
  readonly id: number;
  tytul: string;
  status: Status;
  termin?: string;
}

function wgStatusu(zadania: Zadanie[], status: Status): Zadanie[] {
  return zadania.filter((z) => z.status === status);
}

function zmienStatus(zadanie: Zadanie, nowy: Status): Zadanie {
  return { ...zadanie, status: nowy };   // spread tworzy nowy obiekt, id pozostaje readonly
}

function podsumowanie(zadania: Zadanie[]): Record<Status, number> {
  // zaczynasz od zer, żeby brakujący status też był w wyniku
  const wynik: Record<Status, number> = { nowe: 0, 'w toku': 0, zrobione: 0 };
  for (const z of zadania) wynik[z.status]++;
  return wynik;
}

const lista: Zadanie[] = [
  { id: 1, tytul: 'Powtórzyć pętle', status: 'zrobione' },
  { id: 2, tytul: 'Arkusz 01.2025', status: 'w toku', termin: '2026-11-05' },
  { id: 3, tytul: 'README projektu', status: 'nowe' },
];

console.log(wgStatusu(lista, 'nowe').map((z) => z.tytul));   // ['README projektu']
console.log(podsumowanie(lista));                             // { nowe: 1, 'w toku': 1, zrobione: 1 }
console.log(zmienStatus(lista[2], 'w toku').status);          // 'w toku'
// zmienStatus(lista[2], 'gotowe');  // błąd: '"gotowe"' is not assignable to parameter of type 'Status'
// lista[0].id = 9;                  // błąd: Cannot assign to 'id' because it is a read-only property

Record<Status, number> to wbudowany typ generyczny: obiekt, którego kluczami są wszystkie wartości unii Status, a wartościami liczby. Gdybyś zapomniał jednego klucza w wynik, kompilator by to zgłosił.

Zadanie 3
średnie

Raport z pliku JSON w Node

Napisz skrypt raport.js (moduł ES) uruchamiany poleceniem node raport.js uczniowie.json 3.5, który:

  1. czyta plik podany jako pierwszy argument (fs/promises), a przy braku argumentu wypisuje „Użycie: node raport.js plik.json [próg]” i kończy działanie z kodem 1 (process.exit(1)),
  2. drugi argument to próg średniej (domyślnie 2.0),
  3. dla każdego ucznia wypisuje imię, średnią z dwoma miejscami i słowo „ZAGROŻONY”, jeśli średnia jest poniżej progu,
  4. na końcu zapisuje do zagrozeni.json tablicę imion uczniów poniżej progu,
  5. błąd odczytu pliku (np. zła nazwa) łapie w try/catch i wypisuje czytelny komunikat zamiast stosu wywołań.

Przykładowy plik:

uczniowie.json
[
  { "imie": "Ola", "oceny": [5, 4, 6] },
  { "imie": "Bartek", "oceny": [3, 2, 2, 2] },
  { "imie": "Kasia", "oceny": [] }
]
Pokaż rozwiązanie
raport.js
import { readFile, writeFile } from 'node:fs/promises';

const [plik, progArg] = process.argv.slice(2);
if (!plik) {
  console.error('Użycie: node raport.js plik.json [próg]');
  process.exit(1);                       // kod ≠ 0 oznacza błąd dla systemu / skryptów npm
}
const prog = Number(progArg ?? 2);       // argumenty są napisami

function srednia(oceny) {
  if (oceny.length === 0) return 0;
  return oceny.reduce((suma, o) => suma + o, 0) / oceny.length;
}

try {
  const uczniowie = JSON.parse(await readFile(plik, 'utf8'));
  const zagrozeni = [];

  for (const u of uczniowie) {
    const s = srednia(u.oceny);
    const flaga = s < prog ? ' ZAGROŻONY' : '';
    console.log(`${u.imie.padEnd(10)} ${s.toFixed(2)}${flaga}`);
    if (s < prog) zagrozeni.push(u.imie);
  }

  await writeFile('zagrozeni.json', JSON.stringify(zagrozeni, null, 2));
  console.log(`Zapisano ${zagrozeni.length} imion do zagrozeni.json`);
} catch (e) {
  // ENOENT = brak pliku; SyntaxError = zepsuty JSON
  console.error(`Nie udało się przetworzyć pliku „${plik}”: ${e.message}`);
  process.exit(1);
}

Logikę raportu bez części plikowej możesz sprawdzić tutaj:

raport-logika.js
const uczniowie = [
  { imie: 'Ola', oceny: [5, 4, 6] },
  { imie: 'Bartek', oceny: [3, 2, 2, 2] },
  { imie: 'Kasia', oceny: [] },
];
const prog = Number('3.5');
const srednia = (o) => (o.length ? o.reduce((s, x) => s + x, 0) / o.length : 0);
const zagrozeni = [];
for (const u of uczniowie) {
  const s = srednia(u.oceny);
  console.log(`${u.imie.padEnd(10)} ${s.toFixed(2)}${s < prog ? ' ZAGROŻONY' : ''}`);
  if (s < prog) zagrozeni.push(u.imie);
}
console.log(JSON.stringify(zagrozeni));
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Zadanie 4
trudne

Mini API zadań w node:http

Rozbuduj serwer z lekcji o pełną obsługę listy zadań w pamięci:

  • GET /api/zadania – wszystkie zadania (JSON),
  • GET /api/zadania/2 – jedno zadanie lub 404 z { "blad": "Nie ma zadania 2" },
  • POST /api/zadania – treść żądania to JSON { "tekst": "..." }; serwer nadaje id i zrobione: false, odpowiada 201 i nowym obiektem; pusty tekst → 400,
  • DELETE /api/zadania/2 – usuwa, odpowiada 204 bez treści,
  • obsłuż OPTIONS (preflight CORS), żeby fetch ze strony na innym porcie działał dla POST/DELETE.

Treść żądania w node:http przychodzi strumieniem: zbierz ją przez for await (const czesc of req) dane += czesc;. Przetestuj z przeglądarki frontendem z lekcji 31 albo poleceniem curl -X POST -H "Content-Type: application/json" -d '{"tekst":"Test"}' http://localhost:3000/api/zadania.

Pokaż rozwiązanie
api.js
import { createServer } from 'node:http';

let zadania = [
  { id: 1, tekst: 'Powtórzyć pętle', zrobione: true },
  { id: 2, tekst: 'Zrobić arkusz 01.2025', zrobione: false },
];
let nastepneId = 3;

// pomocnik: odpowiedź JSON z kodem statusu
function odpowiedz(res, status, dane) {
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
  res.end(dane === undefined ? undefined : JSON.stringify(dane));
}

// pomocnik: treść żądania jako obiekt (lub null, gdy nie jest JSON-em)
async function wczytajJson(req) {
  let dane = '';
  for await (const czesc of req) dane += czesc;
  try {
    return JSON.parse(dane || '{}');
  } catch {
    return null;
  }
}

const serwer = createServer(async (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

  if (req.method === 'OPTIONS') {       // preflight – przeglądarka pyta, czy może wysłać POST/DELETE
    res.writeHead(204);
    return res.end();
  }

  const url = new URL(req.url, 'http://localhost');
  const czesci = url.pathname.split('/').filter(Boolean);   // '/api/zadania/2' → ['api', 'zadania', '2']
  const id = czesci[2] !== undefined ? Number(czesci[2]) : null;

  if (czesci[0] !== 'api' || czesci[1] !== 'zadania') {
    return odpowiedz(res, 404, { blad: 'Nie znaleziono' });
  }

  if (req.method === 'GET' && id === null) return odpowiedz(res, 200, zadania);

  if (req.method === 'GET') {
    const z = zadania.find((x) => x.id === id);
    return z ? odpowiedz(res, 200, z) : odpowiedz(res, 404, { blad: `Nie ma zadania ${id}` });
  }

  if (req.method === 'POST' && id === null) {
    const dane = await wczytajJson(req);
    const tekst = typeof dane?.tekst === 'string' ? dane.tekst.trim() : '';
    if (tekst === '') return odpowiedz(res, 400, { blad: 'Pole tekst jest wymagane' });
    const nowe = { id: nastepneId++, tekst, zrobione: false };
    zadania.push(nowe);
    return odpowiedz(res, 201, nowe);
  }

  if (req.method === 'DELETE' && id !== null) {
    const bylo = zadania.length;
    zadania = zadania.filter((x) => x.id !== id);
    return bylo === zadania.length ? odpowiedz(res, 404, { blad: `Nie ma zadania ${id}` }) : odpowiedz(res, 204);
  }

  odpowiedz(res, 405, { blad: 'Niedozwolona metoda' });
});

serwer.listen(3000, () => console.log('API: http://localhost:3000/api/zadania'));

Dane żyją w pamięci – po restarcie serwera wracają do stanu początkowego. Następny krok (poza tą lekcją) to zapis do pliku przez writeFile po każdej zmianie albo baza danych – w Node 24 jest do tego wbudowany, wciąż oznaczony jako eksperymentalny moduł node:sqlite.

Test powtórkowy

Test powtórkowy

Format jak w części pisemnej INF.04: 4 odpowiedzi, 1 poprawna.

0/0poprawnych
Sprawdź się

Co robi kompilator tsc z adnotacjami typów?

Sprawdź się

Która deklaracja jest poprawna w TypeScript?

Sprawdź się

Jaki typ ma zmienna wynik?

function znajdz(lista: string[], szukany: string) {
return lista.find((x) => x === szukany);
}
const wynik = znajdz(['a', 'b'], 'c');
Sprawdź się

Które przypisanie odrzuci kompilator?

type Rozmiar = 'S' | 'M' | 'L';
let r: Rozmiar;
Sprawdź się

Co oznacza ? w deklaracji opiekun?: string wewnątrz interfejsu?

Sprawdź się

Czego NIE ma w Node.js w porównaniu z przeglądarką?

Sprawdź się

Co wypisze skrypt uruchomiony jako node skrypt.js oceny.json?

console.log(process.argv.length, process.argv[2]);
Sprawdź się

Który zapis poprawnie odczytuje plik tekstowy w module ES w Node?

Sprawdź się

W package.json jest skrypt dev: node --watch app.js. Którym poleceniem go uruchomisz?

Sprawdź się

Dlaczego node raport.ts może zakończyć się błędem, mimo że tsc kompiluje plik bez zarzutu?