Powszechne pułapki w JavaScript i TypeScript, które potajemnie psują kod
Pułapki w JavaScript i TypeScript, które wyglądają poprawnie, ale mimo to powodują błędy: porównania z NaN, przymusowa konwersja typów, problemy z czasem wykonywania operacji async/await oraz sprawdzania równości, które wprowadzają w błąd.
Źródła: 20 JavaScript/TypeScript Mistakes That Look Right… But Break Your App
Pułapki związane z porównywaniem i równością
1. NaN nigdy nie jest równe niczemu, nawet samemu sobie.
[1, NaN, 3].indexOf(NaN); // -1, even though NaN is right there
Zgodnie z specyfikacją, NaN === NaN zwraca wartość false, więc każdy mechanizm oparty na ścisłej równości — włączając indexOf — nie będzie w stanie go znaleźć, nawet gdy znajduje się bezpośrednio w tablicy. Zamiast tego należy użyć Number.isNaN(value) lub .includes(), które zostały stworzone specjalnie do prawidłowego radzenia sobie z tym przypadkiem granicznym.
2. typeof null zwraca wartość "object".
if (typeof value === "object") { value.someMethod(); } // value could be null
To stary problem wbudowany w język, który wpada na tych, którzy traktują typeof jako niezawodną metodę sprawdzającą, czy dana wartość jest obiektem. null przechodzi tę weryfikację bez problemu, ale powoduje błąd w momencie próby wywołania na nim metody.
3. Porównywanie obiektów za pomocą JSON.stringify() jest wrażliwe na kolejność.
JSON.stringify({ a: 1, b: 2 }) === JSON.stringify({ b: 2, a: 1 }); // false
Dwa obiekty reprezentujące te same dane mogą zostać zserializowane do ciągów znaków z kluczami w różnej kolejności – dzieje się tak naturalnie po przetworzeniu danych z bazy danych lub innego fragmentu kodu. Unikaj porównywania ciągów znaków i użyj zamiast tego odpowiedniej funkcji do sprawdzania głębokiego równości.
4. .sort() bez funkcji porównywania traktuje wszystko jako tekst.
[10, 2, 1].sort(); // [1, 10, 2]
Wartości są najpierw przekształcane w łańcuchy, a następnie porównywane znak po znaku, co zakłóca prawidłowe uporządkowanie liczbowe. Zawsze używaj wyraźnej funkcji porównywania dla liczb, np. (a, b) => a - b.
Przymus typu
5. Operator + przechodzi na łączenie wartości w momencie, gdy jeden z operandów jest łańcuchem.
function total(price, quantity) {
return price + quantity; // "5" + 3 = "53" if price came from a form input
}
Wartości pochodzące z parametrów zapytania, pól formularza lub skonwertowanego JSON często pozostają łańcuchami, mimo że wyglądają jak liczby. Operator + nie przekształci dla ciebie łańcucha w liczbę — zamiast tego przekształca liczbę w łańcuch.
6. Wartości domyślne przy destrukcji obiektów mają zastosowanie tylko wtedy, gdy właściwość jest undefined, nigdy przy wartości null.
function greet({ name = "friend" } = {}) { return `Hi, ${name}`; }
greet({ name: null }); // "Hi, null" — the default never kicks in
Wyraźne przekazanie null jest traktowane jako podanie wartości, a nie jej pominięcie, więc domyślna opcja awaryjna jest całkowicie pomijana. Jeśli wywołujący mogą legalnie wysłać null, musisz sprawdzić to osobno.
Czas wykonywania asynchronicznego
7. Wykonywanie .filter(Boolean) na tablicy obietnic nie przynosi żadnych efektów.
const results = items.map((item) => checkAsync(item)); // array of Promise objects
const valid = results.filter(Boolean); // every Promise is truthy, filters nothing
Instancja Promise zawsze jest wartością prawdziwą, niezależnie od tego, do czego ostatecznie się rozwiąże lub zostanie odrzucona. Najpierw musisz je rozwiązać za pomocą await Promise.all(results), a dopiero potem filtrować uzyskane wartości.
8. Łańcuchowanie opcjonalne zatrzymuje obliczanie całego wyrażenia, a nie tylko części opcjonalnej.
user?.getPermissions().includes("admin"); // if user is null, this is undefined, not an error, but also never calls .includes at all
Może się wydawać, że pomija się tylko segment ?., ale wszystko, co jest po nim w łańcuchu, również przestaje działać. Zazwyczaj jest to pożądane zachowanie, choć staje się subtelnym błędem, gdy ostatnia wywołanie – np. do logowania lub analizy danych – nie jest wykonywane w ogóle.
9. Promise.all odrzuca wszystkie wyniki, jak tylko choćby jedna obietnica zostanie odrzucona.
const [a, b, c] = await Promise.all([taskA(), taskB(), taskC()]);
// if taskC rejects, you get nothing back from taskA and taskB either, even if they succeeded
To jest celowe zachowanie typu „wszystko albo nic”. Gdy w twojej sytuacji dopuszczalny jest częściowy sukces, Promise.allSettled zwraca wynik każdej obietnicy – niezależnie od tego, czy się powiodła, czy nie – zamiast odrzucać udane wyniki tylko dlatego, że jedno zadanie zawiodło.
Mutacja obiektów i tablic
10. const blokuje zmienną, a nie obiekt, na który wskazuje.
const config = { retries: 3 };
config.retries = 10; // completely legal, const never protected this
Użycie słowa kluczowego const zapobiega jedynie przypisaniu tej zmiennej nowej wartości. Obiekt, do którego odnosi się ta zmienna, pozostaje równie modyfikowalny jak w przypadku użycia let. Jeśli potrzebujesz rzeczywistej niezmienności, użyj Object.freeze() — pamiętaj jednak, że metoda ta mrozi strukturę tylko na jednym poziomie.
11. .sort() modyfikuje oryginalny tablicę, w odróżnieniu od .map() czy .filter().
const original = [3, 1, 2];
const sorted = original.sort();
console.log(original); // [1, 2, 3] — also changed, probably not what was intended
Latwo założyć, że wszystkie metody tabliczne zwracają nową tablicę, ponieważ większość popularnych z nich faktycznie to robi. .sort() i .reverse() są wyjątkami, a modyfikacja tablicy, do której inne części kodu nadal mają odniesienie, powoduje klasyczny błąd „ta wartość zmieniła się sama z siebie”.
12. for...in iteruje również nad właściwościami odziedziczonymi, a nie tylko nad własnymi kluczami obiektu.
Array.prototype.customMethod = function () {};
const arr = [1, 2, 3];
for (const key in arr) { console.log(key); } // includes "customMethod" too
W praktyce rzadko stanowi to problem, ponieważ rozszerzanie wbudowanych prototypów jest nietypowe, ale każda wyliczalna właściwość odziedziczona z łańcucha prototypów pojawi się w pętli for...in. Użycie Object.keys() lub for...of całkowicie unika takich niespodzianek.
Funkcje i zakres
13. Funkcje strzałkowe nigdy nie wiążą swojego własnego this.
const counter = {
count: 0,
increment: () => { this.count++; }, // `this` here is NOT the counter object
};
Funkcja strzałkowa pobiera wartość this z zakresu leksykalnego, w którym została napisana, a nie z obiektu, który ją wywołuje. Dzięki temu są one idealne do zwracanych funkcji, które muszą zachować dostęp do zewnętrznego this, ale stanowią kiepski wybór dla metod obiektów lub klas, które powinny odwoływać się do swojej własnej instancji.
14.Instrukcja switch przechodzi dalej bez żadnych efektów, gdy pominie się break.
switch (status) {
case "pending":
console.log("Order is pending");
case "shipped": // no break above, this runs too
console.log("Order has shipped");
}
Pominięcie jednego break powoduje wykonywanie wszystkich kolejnych przypadków, niezależnie od tego, czy ich warunki faktycznie się spełniły. Celowe przechodzenie dalej zdarza się czasami, ale jest na tyle rzadkie, że brak break prawie zawsze wskazuje na błąd, a nie na decyzję projektową.
Data i liczby
15. 0.1 + 0.2 to nie 0.3.
0.1 + 0.2 === 0.3; // false
0.1 + 0.2; // 0.30000000000000004
Jest to spowodowane standardowym sposobem reprezentacji liczb zmiennoprzecinkowych i dotyczy niemal wszystkich języków, a nie jest specyficzne dla JavaScript. Unikaj porównywania ścisłego równości wartości dziesiętnych obliczonych dynamicznie; zamiast tego sprawdź, czy różnica między dwiema liczbami mieści się w akceptowalnej tolerancji.
16. Proste łańcuchy dat są interpretowane jako UTC, natomiast daty oddzielone kreskami są interpretowane według czasu lokalnego.
new Date("2024-01-01"); // midnight UTC
new Date("01/01/2024"); // midnight local time
Dwa łańcuchy, które wydają się opisywać ten sam dzień, mogą odpowiadać różnym datom kalendarzowym w zależności od strefy czasowej osoby uruchamiającej kod. Wybierz jeden format dat i trzymaj się go konsekwentnie, korzystając z dobrze sprawdzonej biblioteki do obsługi dat zamiast ręcznego parsowania łańcuchów.
Specyficzne pułapki w TypeScript
17. Aserty typów nie walidują niczego w czasie wykonywania.
const user = JSON.parse(rawData) as User; // TypeScript trusts you, nothing is actually checked
Zapisanie as User mówi kompilatorowi, aby w pełni ci zaufał, i robi dokładnie to, nie przeprowadzając żadnej weryfikacji, czy zinterpretowana wartość rzeczywiście ma formę, którą twierdzisz. Złe lub nieoczekiwane dane nadal trafiają do twojego programu jako zweryfikowane, aż w końcu powodują błąd gdzieś dalej w łańcuchu przetwarzania.
18. Operator stwierdzenia niezerowości tylko tłumi ostrzeżenia, nie zmienia rzeczywistości.
function getName(user?: User) {
return user!.name; // TypeScript stops warning you; the runtime null check is still gone
}
Dodanie ! informuje TypeScript, że jesteś pewien, iż wartość nie jest ani null, ani undefined. Jeśli to założenie jest błędne, kompilator po prostu przestaje sygnalizować to ryzyko, a błąd pojawi się w czasie wykonywania, dokładnie tam, gdzie system typów miał go wcześniej wykryć.
19. Sprawdzanie nadmiarowych właściwości dotyczy tylko literałów obiektowych przekazywanych bezpośrednio, a nie zmiennych o tej samej strukturze.
type Config = { retries: number };
function setup(config: Config) {}
setup({ retries: 3, timeout: 5000 }); // error: 'timeout' does not exist on type Config
const options = { retries: 3, timeout: 5000 };
setup(options); // no error — same excess property, but it slips through
Przekazuj literal obiektu bezpośrednio do wywołania funkcji, a TypeScript sprawdza każdą właściwość, odrzucając wszystko co dodatkowe. Najpierw przypisz ten sam obiekt do zmiennej, a następnie przekazuj tę zmienną – wtedy sprawdzenie zupełnie znika. To rzeczywisty dziwactwo w zachowaniu sprawdzacza typów, co oznacza, że błąd pisowni lub niepotrzebne pole mogą po cichu ominąć weryfikację, w zależności wyłącznie od sposobu, w jaki wartość trafiła do funkcji.
20. Liczbowe enumy przyjmują dowolne liczby, a nie tylko deklarowane elementy.
enum Status { Pending, Shipped, Delivered }
function process(status: Status) {}
process(999); // no compile error, even though 999 isn't a declared Status value
W rzeczywistości liczbowe enumy w TypeScript to po prostu zwykłe liczby, więc system typów przyjmie każdą wartość liczbową jako ważną, niezależnie od tego, czy odpowiada ona deklarowanemu elemencie. Enumy stringowe wprowadzają tu rzeczywiste ograniczenia, więc gdy chcesz, aby kompilator faktycznie odrzucił błędne dane, wybierz enumy stringowe lub unię utworzoną z literówek stringowych.
Żaden z tych dwudziestu problemów nie jest trudny do zrozumienia ani nietypowy. Każdy z nich występuje na tyle często w rzeczywistym, sprawdzonym i wydawanym kodzie, że przypomina zupełnie zwykły JavaScript lub TypeScript, dopóki konkretny wprowadzony danych, określony moment lub przypadek krawędziowy go nie ujawni. Znajomość tej listy nie zastąpi konieczności dokładnego testowania kodu. Daje ona jedynie przewagę: możesz sprawdzać problemy, o których już wiesz, że należy uważać, zamiast natknąć się na nie później w środowisku produkcyjnym.
Literatura pokrewna
- Dziesięć hiddenowych potknięć w componentach React, które spowalniają nowoczesne aplikacje — Poznaj dziesięć powszechnych błędów w componentach React – od braków w semantycznym HTML po brak memoizacji – oraz rozwiązania potrzebne, aby aplikacje pozostały szybkie, dostępne i wolne od błędów do 2026 roku.
- Cechy Node.js 26, które cicho zastępują lata workaroundów — Przegląd API Temporal w Node.js 26, natywnego wykonywania TypeScript, narzędzi do zarządzania pamięcią cache oraz innych ulepszeń, które eliminują długo istniejące rozwiązania tymczasowe.