Po co w ogóle myśleć o migracji z JavaScript na TypeScript w dużym repo?
Cel jest prosty: odpowiedzieć, czy migracja z JavaScriptu na TypeScript w dużym repozytorium ma sens finansowy i techniczny, i jeśli tak – zrobić to tak, żeby nie rozwalić roadmapy, nie zajechać zespołu i nie utopić się w długu typów. Chodzi o to, by dopasować typowanie do produktu, a nie produkt do typowania.
Czy migracja z JavaScript na TypeScript ma sens w dużym repo?
Realne korzyści dla dużego projektu – nie tylko „ładniejsze typy”
W małych projektach TypeScript bywa miłym dodatkiem. W dużych monorepo, wieloletnich aplikacjach i produktach SaaS staje się narzędziem kontroli ryzyka. Kluczowe zyski z migracji z JavaScript do TypeScript w monorepo to:
- Stabilność zmian – kompilator wyłapuje całe klasy błędów, które w czystym JS wychodzą dopiero w runtime albo w produkcji. Zwłaszcza przy refaktoryzacjach „przekopujących” modele danych i API wewnętrzne.
- Szybszy onboarding nowych osób – typy pełnią funkcję żywej dokumentacji. Nowy dev widzi od razu, jakich danych oczekuje funkcja i co zwraca, zamiast zgadywać z nazw lub skakać po plikach.
- Bezpieczniejsze refaktoryzacje – przy zmianach w warstwie domenowej, kontraktach API czy bibliotekach współdzielonych kompilator „ciągnie” dewelopera za rękę przez wszystkie miejsca, które trzeba zaktualizować.
- Lepsze wsparcie narzędzi – IDE dostaje pełną wiedzę o strukturze kodu: autouzupełnianie, nawigacja (go to definition), rename symbol, automatyczne importy – to nie jest kosmetyka, tylko realne skrócenie czasu pracy.
- Kontrola nad długiem technicznym – TypeScript zmusza do jawnego określania granic modułów i modeli danych. Z czasem mniej jest „magicznych” helperów, które robią wszystko i nic, a więcej przewidywalnych kontraktów.
W dużym repo nawet niewielkie procentowo zmniejszenie liczby błędów produkcyjnych albo skrócenie czasu refaktoryzacji o kilkanaście procent przekłada się na wymierne oszczędności i mniejszą liczbę „awaryjnych” sprintów.
Kiedy migracja daje największy zwrot z inwestycji
Największy sens biznesowy ma TypeScript tam, gdzie produkt:
- Ma żyć latami – długoterminowe produkty SaaS, platformy B2B, systemy wewnętrzne firm, które są rozwijane zamiast przepisywane co dwa lata.
- Często się zmienia – dynamicznie rozwijane aplikacje, wiele eksperymentów, feature flagi. Każda zmiana w modelu danych czy API niesie wtedy większe ryzyko regresji.
- Ma dużo integracji – bramki płatności, zewnętrzne API, wewnętrzne mikroserwisy, rozbudowane front-back kontrakty. Typy pomagają nie pomylić się w kształcie danych.
- Jest rozwijany przez duży zespół – rotacja ludzi, kilka squadów, różne poziomy doświadczenia. Typy stabilizują wspólny język w projekcie.
Migracja ma wysoki ROI zwłaszcza w projektach, gdzie:
- często dochodzi do regresji na granicach modułów,
- brakuje spójnej dokumentacji modeli danych,
- zmiany w jednym module wywołują niespodziewane efekty uboczne w innych miejscach.
Kiedy lepiej zostać przy dobrym JavaScripcie, JSDoc i ESLint
TypeScript nie jest darmowy. Wymaga nauki, utrzymania konfiguracji, aktualizacji wersji. Są sytuacje, w których rozsądniej jest zostać przy dobrze ogarniętym JavaScripcie:
- Krótko żyjący projekt – jednorazowe kampanie marketingowe, proste landingi, małe narzędzia „na chwilę”. Pełna migracja tam nie spłaci się nigdy.
- Brak podstaw porządnego JS – jeśli w projekcie nie ma sensownych testów, ESLint nie przechodzi, a style kodu są przypadkowe, to wrzucenie TypeScriptu zrobi tylko większy bałagan.
- Zespół jest przeciążony – gdy ludzie gaszą pożary codziennie, dorzucenie im nauki TS i migracji może po prostu wywołać bunt i spadek jakości.
- Stack jest ekstremalnie dynamiczny – mocno meta-programistyczne biblioteki, dużo eval, generowany kod, pluginy pisane przez zewnętrzne zespoły – TypeScript tam będzie się mocno pocił.
W takich przypadkach sens mają „lżejsze” warianty: JSDoc + ESLint + dobre konwencje. Można opisywać typy w komentarzach, zachować płynność JS, a mimo to mieć auto-uzupełnianie i minimalną kontrolę typów z checkJs.
Jak ocenić dojrzałość repo i zespołu do migracji
Zanim ktokolwiek zacznie przepinać rozszerzenia plików na .ts, przydaje się krótka ocena „czy my w ogóle jesteśmy na to gotowi”. Praktycznie, wystarczy odpowiedzieć na kilka pytań:
- Testy: czy projekt ma sensowne testy jednostkowe/integracyjne, choćby częściowe? Bez testów każda większa zmiana (w tym migracja) jest hazardem.
- Standardy kodu: czy istnieje ustalony styl (ESLint, Prettier), czy każdy pisze po swojemu? TypeScript dorzuca kolejną oś standardów do ogarnięcia.
- Rotacja ludzi: jeśli co chwila ktoś odchodzi, a nowi wchodzą w środek chaosu, typy mogą pomóc, ale jednocześnie podniosą próg wejścia. Tu kluczowe jest dobre przeszkolenie.
- Decyzyjność: czy ktoś (lead, architekt) realnie „trzyma” kierunek techniczny? Migracja bez osoby odpowiedzialnej za decyzje strategiczne skończy się mnóstwem rozjechanych podejść.
Jeśli większość odpowiedzi jest negatywna, lepiej zacząć od porządków w JavaScript – ustawić linting, ogarnąć testy na newralgicznych ścieżkach – i dopiero potem planować TypeScript.
Diagnoza wyjściowa – audyt projektu przed decyzją
Szybki przegląd architektury i skali repozytorium
Na początek warto z grubsza zmapować, z czym mamy do czynienia. Kluczowe pytania:
- Typ repo: monorepo (Nx, Turborepo, pnpm workspaces) czy wiele osobnych repozytoriów?
- Liczba pakietów: ile jest pakietów / aplikacji, ile z nich jest aktywnie rozwijanych?
- Poziom „legacy”: gdzie siedzą najstarsze, najmniej ruszane fragmenty, a gdzie dzieje się produktowo najwięcej?
W praktyce dużo mówi proste spojrzenie na strukturę katalogów:
src/legacy,old/,v1/– często zlepki starego kodu bez testów, z dynamicznymi hackami.src/modules,packages/– typowe moduły biznesowe, zwykle lepszej jakości.shared/,common/– współdzielone utilsy i modele; idealni kandydaci na pierwsze typowanie, ale też potencjalne źródło ogromnej ilości błędów przy nierozważnej migracji.
Kluczowe wskaźniki: testy, dynamika kodu, „any-like” wzorce
Przed migracją warto zmierzyć kilka rzeczy, choćby bardzo orientacyjnie:
- Pokrycie testami – nie muszą to być procenty z dokładnością do jednego miejsca po przecinku. Ważniejsze jest, na których modułach w ogóle są testy, a gdzie ich kompletnie brak.
- Dynamiczny kod – ile jest miejsc typu
obj[key],data[someRuntimeField], manipulacje losowymi strukturami z API, które zmieniają się w locie. Im bardziej dynamicznie, tym drożej będzie typować. - „Any-like” wzorce – funkcje przyjmujące i zwracające „wszystko”, brak jasnych interfejsów, funkcje utili z dwudziestoma opcjonalnymi parametrami.
Im więcej takich miejsc, tym bardziej widać, że projekt woła o jawne modele danych. Równocześnie koszt migracji będzie wyższy – trzeba brać to pod uwagę przy skalowaniu planu.
Inwentaryzacja narzędzi – bundler, testy, CI/CD
TypeScript musi się wpasować w istniejący łańcuch narzędzi. Krótka checklista:
- Bundler: Webpack, Vite, Rollup, Parcel? Każdy ma inny sposób integracji, inne pluginy do TS.
- Transpilacja: obecnie Babel? Esbuild? Surowy
tscdo JS? Trzeba zdecydować, czy TypeScript będzie służył tylko do typów, czy też do transpilacji. - Test runner: Jest, Mocha, Vitest, Playwright? Wszystkie wymagają konfiguracji dla TS (transformery, ts-node, prekompilacja).
- Linter: ESLint (z jakimi pluginami?), może jeszcze stare TSLint w jakichś zakamarkach?
- CI/CD: gdzie i jak budowany jest projekt, czy pipeline ma jeszcze zapas czasu, czy już chodzi na granicy limitów?
Ta inwentaryzacja pomaga oszacować, ile czasu zajmie sama konfiguracja infrastruktury pod TypeScript zanim jeszcze ruszy migracja kodu.
Ocena ryzyka – które moduły są krytyczne biznesowo
Nie każdy moduł ma tę samą wagę. Zwykle są obszary, których nikt nie chce ruszać w piątek wieczorem:
- Moduły płatności, billing, rozliczenia – drobny błąd, i zaczynają się reklamacje, chargebacki, straty przychodu.
- Autoryzacja i uprawnienia – jeden bug i użytkownicy widzą cudze dane.
- Kluczowe integracje – np. przesyłanie danych do partnera, który ma twarde SLA i kary umowne.
Te części warto typować jak najszybciej, bo każdy błąd tam jest kosztowny, ale sama migracja powinna być robiona ostrożnie: z dobrą siatką testów, w minimalnych krokach, najlepiej z pilotażem na peryferyjnych fragmentach.
Przykładowa checklist audytu przedmigracyjnego
Prosta, praktyczna lista do przejścia przed decyzją „migrujemy”:
- Sprawdzone: główne moduły, mapka architektury (co z czym gada).
- Oznaczone: moduły krytyczne vs peryferyjne.
- Oszacowane: gdzie są testy, gdzie ich nie ma.
- Rozpoznane: stos narzędzi – bundler, test runner, linter, CI.
- Określone: cele migracji (np. „typy dla kontraktów API i integracji do końca Qx”).
- Wskazany: właściciel techniczny migracji (lead / architekt).
Modele migracji – od „tanio i ostrożnie” do „twarde cięcie”
Trzy podstawowe strategie migracji
W dużych projektach da się wyróżnić trzy sensowne podejścia do migracji z JavaScript do TypeScript w monorepo:
- Big-bang – jednorazowe, większe przejście:
- Zmieniasz rozszerzenia wielu plików na
.ts/.tsx, włączasz ostrzejszą konfigurację TS, równolegle naprawiasz błędy typów. - Bardzo ryzykowne w dużych, żywych projektach. Sens ma tylko przy krótkim kodzie, dobrych testach i „zamrożonej” roadmapie.
- Zmieniasz rozszerzenia wielu plików na
- Stopniowa migracja per moduł:
- Ustalasz priorytet modułów (np. od najbardziej współdzielonych / krytycznych) i migrujesz je po kolei.
- JavaScript i TypeScript żyją obok siebie, konfiguracja
allowJsicheckJspozwala na płynne przejście.
- Hybryda z JSDoc:
- Najpierw dodajesz typy przez JSDoc w istniejących plikach
.js, dopiero później przepinasz wybrane części na.ts. - Koszt wejścia jest niższy, bo nie ruszasz bundlera i nie dotykasz zbyt wielu plików naraz.
- Najpierw dodajesz typy przez JSDoc w istniejących plikach
Jak dobrać strategię do budżetu, czasu i presji biznesu
Przy wyborze strategii kluczowe są trzy liczby: dostępny czas, budżet i tolerancja na ryzyko. Prosty scenariusz decyzyjny:
- Jeśli produkt musi dowozić feature’y bez przerw, a zespół jest obciążony – wybór praktyczny to stopniowa migracja + JSDoc. TS wchodzi na obrzeża, nie blokuje codziennego developmentu.
Kiedy Big-bang ma jeszcze sens, a kiedy już jest sabotażem
Czasem presja z góry jest prosta: „zróbmy to raz, a porządnie, przechodzimy wszędzie na TypeScript”. Na papierze kusi, bo potem wszystko jest już „czyste”. W praktyce big-bang ma sens tylko w kilku scenariuszach:
- kod jest relatywnie krótki i jednorodny (np. jeden główny serwis, kilka modułów, brak rozległego frontu i dziesiątek mikro-apek);
- istnieją dobre testy regresyjne i smoke testy, odpalane automatycznie w CI;
- produkt ma okno czasowe, w którym można zamrozić nowe funkcje na 1–2 sprinty;
- zespół ma choć jedno–dwa mocniejsze „silniki” TS, które pociągną resztę przy naprawianiu błędów typów.
Jeśli choć dwóch z powyższych punktów nie da się odhaczyć, big-bang zwykle kończy się tygodniami gaszenia pożarów. Pojawia się też typowy anty-wzorzec: wymuszone użycie any wszędzie, byle tylko „projekt się budował”. Efekt – TypeScript jest, ale realna kontrola typów żadna, tylko dodatkowa złożoność.
Bezpieczniejszy wariant big-bangu to „big-bang konfiguracji, małe kroki w kodzie”: konfigurujesz TypeScript, bundler, lintery i CI w jednym, dobrze zaplanowanym rzucie, ale samą migrację plików rozciągasz na tygodnie. Dzięki temu koszt narzędziowego przejścia płacisz raz, a ryzyko funkcjonalne rozbijasz na serię małych PR-ów.
Stopniowa migracja per moduł – jak unikać „wiecznej bety”
Model „migrujemy stopniowo” ma jedną pułapkę: jeżeli nie ma czytelnych zasad, po roku zespół dalej ma 70% JS i ciągły spór „czy i kiedy to przepiszemy”. Żeby tego uniknąć, dobrze jest:
- zdefiniować konkretny cel procentowy (np. „min. 60% linii w TS do końca roku” albo „wszystkie moduły
shared/iapi/w TS do końca Qx”); - uzgodnić prostą regułę „boy scouta”: jeśli dotykasz pliku JS powyżej np. 30–50 linii zmian, spróbuj przepiąć go na TS, chyba że jest ku temu bardzo dobry powód;
- trzymać krótką listę modułów „off-limits”, których na razie nie ruszacie (np. bardzo niestabilne integracje, które i tak będą przepisywane produktowo).
Przy takim podejściu migracja nie jest osobnym projektem, tylko „przy okazji” towarzyszy normalnemu developmentowi. Koszt rozkłada się w czasie, a biznes nie ma poczucia, że tygodniami płaci za „brak nowych funkcji”.
Hybryda z JSDoc – tani bufor na zespoły z małym doświadczeniem w TS
Jeśli ludzie w zespole nie mają mocnego doświadczenia z typami, wejście od razu w pełny TypeScript potrafi zablokować prace. W takiej sytuacji JSDoc jest rozsądnym kompromisem:
- daje podpowiedzi w IDE i wczesne błędy typów bez zmiany rozszerzeń plików;
- łatwo go stopniowo wprowadzać: od prostych
@param,@returns, przez@typedefdla modeli domenowych; - wymaga minimalnych zmian w bundlerze, czasem wystarczy ustawienie
checkJswtsconfig.json.
Taka hybryda ma sens jako faza przejściowa 3–6 miesięcy. Potem albo zespół jest już na tyle „ograny”, że śmiało przepina część modułów na TS, albo uczciwie trzeba powiedzieć, że na ten moment budżet mentalny i czasowy na mocniejsze typowanie się nie spina.

Konfiguracja TypeScript pod istniejący projekt – nie strzelać z armaty
Startowy tsconfig.json dla legacy – profil „minimum dramatów”
Domyślna konfiguracja generowana przez tsc --init jest często zbyt ogólna albo zbyt ostra dla dużych, starych projektów. Rozsądny, „budżetowy” punkt startowy dla migracji wygląda mniej więcej tak:
{
"compilerOptions": {
"target": "ES2019",
"module": "commonjs",
"strict": false,
"noImplicitAny": false,
"strictNullChecks": false,
"allowJs": true,
"checkJs": true,
"jsx": "react-jsx",
"esModuleInterop": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"baseUrl": "./",
"paths": {
"@shared/*": ["src/shared/*"],
"@api/*": ["src/api/*"]
},
"noEmit": true
},
"include": ["src"],
"exclude": ["dist", "build", "node_modules"]
}
To nie jest „idealny” zestaw flag, ale dobry kompromis na pierwsze tygodnie. Kluczowe jest tu noEmit: true – TypeScript służy wtedy wyłącznie do sprawdzania typów, a produkcyjny build nadal robi Babel/Webpack/Vite. Unikasz w ten sposób podwójnej komplikacji (nowe typy + nowe miejsce transpilacji).
Stopniowe zaostrzanie reguł zamiast jednego szoku
Zamiast włączać od razu "strict": true, lepiej zaplanować kilkustopniowe „dokręcanie śruby”:
- Faza 1:
allowJs+checkJs, bezstrict. Zespół oswaja się z błędami typów jako „warningami jakościowymi”. - Faza 2: włączasz
noImplicitAnydla nowych modułów (np. przez osobnytsconfigwpackages/new-app), reszta repo nadal działa na łagodnych ustawieniach. - Faza 3: selektywnie włączasz
strictNullChecksw tych pakietach, które mają lepsze testy i jasne modele danych, np.@shared/models.
Takie podejście upraszcza rozmowę z biznesem: jasno widać kolejne „progi kosztów” i można je powiązać z milestone’ami produktowymi. Zamiast jednego, bolesnego sprintu refaktoryzacji masz kilka mniejszych, wplatanych pomiędzy normalne zadania.
Integracja z bundlerem – najtańsze scenariusze
Najbardziej czasochłonny wariant to pełne przejście z Babela na tsc w roli kompilatora. Dla większości zespołów taniej jest:
- pozostawić Babel / esbuild jako główne narzędzie transpilacji ESNext → ES5/ESNext,
- użyć TypeScript wyłącznie jako checker typów, odpalany osobno w CI i lokalnie.
Przykładowo w projekcie z Webpackiem:
ts-loaderlubbabel-loaderz presetem@babel/preset-typescriptogarniają JS/TS podczas bundlowania,- komenda
tsc -p tsconfig.json --noEmitdziała jako osobny krok w CI (osobny job lub krok przed testami).
W wielu przypadkach to wystarcza na długie miesiące. Pełna migracja na tsc jako jedyne źródło prawdy ma sens głównie w back-endach (Node), gdzie bundlowanie jest prostsze, a zysk z jednego narzędzia jest większy niż na złożonym froncie.
Testy i TypeScript – nie przepłacać na starcie
Integracja runnerów testów z TS też da się zrobić „na miękko”:
- dla Jesta – użycie
ts-jestalbo przedkompilacja kodu testowego (np. osobny kroktscdla katalogutests); - dla Vitesta – wsparcie TS jest natywne przy konfiguracji Vite, czasem wystarczy dopięcie
tsconfigi aliasów.
Najtańszy model: większość testów zostaje w JS, a TypeScript jest „po drodze” jedynie przy importowaniu typowanych modułów. Migracja samych testów do TS ma sens dopiero wtedy, gdy wchodzisz w bardziej agresywne sprawdzanie typów (np. strictNullChecks) albo zaczynasz udostępniać typowane helpery testowe, z których korzysta wiele pakietów.
Strategia „szybkich wygranych” – gdzie typy przynoszą efekt najszybciej
Największy zwrot z inwestycji: kontrakty API i modele domenowe
Jeśli trzeba wybrać jedno miejsce, w którym typy najczęściej „spłacają się” w pierwszym miesiącu, są to kontrakty API i modele danych. Tam zwykle siedzi najwięcej błędów:
- błędne założenia co do pól (np.
user.address.cityzawsze istnieje – aż wpadnie użytkownik z danymi z innego kraju), - pozorne „opcjonalne” właściwości, które w praktyce są obowiązkowe, tylko nie zostało to wyrażone,
- kilka wariantów tego samego obiektu (np. „user z API”, „user w UI”, „user w bazie”) bez jasnego opisania.
Wprowadzenie typów/typedefów np. w src/api/types.ts rozwiązuje część problemów w ciemno – IDE zaczyna wołać, gdy ktoś źle użyje modelu. Koszt: kilka godzin na przepisanie istniejących interfejsów JSON, zysk: mniej bugów kategorii „undefined is not a function / property of undefined”.
Współdzielone utilsy – mały nakład, duży zasięg
Drugim kandydatem na szybkie wygrane są moduły współdzielone: shared/, utils/, common/. Każda funkcja używana w kilkunastu miejscach to dobry cel:
- raz zdefiniowany typ wejścia/wyjścia od razu podnosi jakość kilkudziesięciu wywołań;
- błędy w utilach zwykle mają efekt kaskadowy, więc ich „uszczelnienie” zmniejsza liczbę dziwnych regresji;
- te pliki są często krótsze i mniej zależne od frameworków, więc typuje się je szybciej niż złożone komponenty.
Praktyczny trik: w module shared zacząć od najmniejszych, czysto funkcyjnych utili (np. formatery dat, walidacje, proste mapowania), a dopiero potem przechodzić do większych helperów zależnych od frameworka czy środowiska.
Edge-case’owe moduły produkcyjne: płatności, logowanie, uprawnienia
Wysokie ryzyko + relatywnie ograniczony zakres kodu = świetny kandydat na wczesne typowanie. Moduły takie jak płatności, sesje czy uprawnienia mają zwykle:
- dobrze zdefiniowane ścieżki biznesowe,
- twarde wymagania co do danych (np. token musi mieć określone pola),
- nieproporcjonalnie wysoką cenę błędu.
Nawet częściowe wprowadzenie typów (np. tylko struktury tokenów, payloadów webhooków) może zmniejszyć liczbę krytycznych incydentów. Koszt jest zwykle umiarkowany, bo te moduły są mniejsze od głównego UI, a każda ujawniona niespójność modelu danych jest biznesowo „twardym” argumentem za migracją.
Typy w granicach modułów – tani sposób na ograniczenie „promieniowania” błędów
Jeżeli nie ma budżetu na pełne przetypowanie środka modułów, da się zrobić prostszy, a i tak użyteczny ruch: opisać typami granice. Chodzi o:
- sygnatury funkcji eksportowanych z modułów,
- typy propsów w komponentach React/solid/svelte (cokolwiek jest w użyciu),
- interfejsy dla serwisów (np.
UserService,AuthClient).
Wnętrze może dalej być luźnym JS-em. Kluczowe jest, że to, co „wychodzi na zewnątrz”, ma konkretny kształt. Dzięki temu błędy nie rozlewają się po całym repo, tylko zatrzymują na granicy modułu. Taki wariant często jest do wdrożenia w jednym–dwóch sprintach bez wielkiego bólu.
Typowanie legacy kodu bez przepisywania wszystkiego
Strategia „wrapper zamiast przeróbki środka”
Stary kod bywa tak kruchy, że nikt nie chce go dotykać. Zamiast wyważać drzwi, można go owinąć typowanym wrapperem:
- tworzysz nowy moduł TS, który wystawia sensowny, dobrze opisany typami interfejs (np.
calculateInvoiceTotal(invoice: Invoice): Money), - w środku wrappera przekazujesz dane do istniejącego, legacy JS i odpowiednio normalizujesz wynik,
- reszta systemu korzysta już tylko z wrappera, nie z surowego legacy.
To podejście szczególnie dobrze działa w przypadku dużych, niejasnych funkcji „god objects”. Nie musisz od razu rozumieć całego ich wnętrza – wystarczy rzetelnie opisać wejście i wyjście, ewentualnie kilka kluczowych wyjątków.
any jako narzędzie, nie śmietnik
Bez any migracja dużego legacy jest w praktyce niewykonalna, ale trzeba go traktować jak tymczasową protezę, a nie domyślne rozwiązanie:
- używaj
anyprzede wszystkim na granicach z nieprzetypowanym światem (zewnętrzna biblioteka, stary moduł),
Stopniowe uszczelnianie typów zamiast heroicznej refaktoryzacji
Legacy zwykle nie psuje się w jednym miejscu, tylko „przecieka” na wielu poziomach. Zanim ktoś rzuci hasło „przepiszmy wszystko na nowo”, da się wdrożyć kilka tanich kroków uszczelniających istniejący kod:
- lokalne
@ts-ignorez komentarzem – blokujesz pojedynczy błąd typów, ale wymagasz od autora dopisku, dlaczego to jest akceptowalne (np.// @ts-ignore: zewnętrzna biblioteka źle otagowana); - system TODO na typy – prosty komentarz w stylu
// TODO[types]: doprecyzować typ OrderStatuspozwala w przyszłości zebrać „długi typowe” grepem lub osobnym skryptem; - oznaczanie ryzykownych fragmentów – np.
type UnsafeJson = any;i świadome używanie tego aliasu tylko tam, gdzie dane są realnie niekontrolowane.
Efekt jest taki, że nawet bez dużej refaktoryzacji tworzy się mapa miejsc, do których warto wrócić w kolejnych iteracjach. Zespół przestaje chować problemy typów pod dywan, ale też nie blokuje całego sprintu przez jeden moduł, którego nikt nie ogarnia.
Zamiana dynamicznej magii na jawne typy krok po kroku
Największy wróg TypeScriptu w legacy to dynamiczne konstrukcje – obiekty składane „w locie”, monkey patching, dostęp po obj[key] z luźnym kluczem. Pełna eliminacja bywa kosztowna, ale da się schodzić z tej ścieżki warstwami:
- Nazwane typy zamiast anonimowych struktur – zamiast zwracać z funkcji „gołe” obiekty, tworzysz choćby bardzo prosty
type/interface. Nawet jeśli część pól dostaje typany, sam fakt nadania nazwy ułatwia dalsze uszczelnianie. - Stopniowe zawężanie indeksów – gdy masz
obj[key: string], szukasz w kodzie faktycznie używanych kluczy i zamieniasz na unię literalną, np.Record<"pending" | "paid" | "cancelled", Invoice[]>. Nie musisz załatwić wszystkich na raz – zacznij od kilku najbardziej krytycznych. - Owijanie „magii” helperami – jeżeli logika mocno polega na dostępie dynamicznym, zamykasz go w małej funkcji z typowanym interfejsem, a reszta kodu wywołuje już tylko tę funkcję.
To powolne oswajanie „magii”, ale z bardzo korzystnym bilansem wysiłek/efekt. Nie przepalasz tygodnia na walkę z całym modułem, tylko wyciągasz po jednym klocku i nadajesz mu lepszy kształt typów.
Refaktoryzacja przy okazji – typy jako pretekst, nie główny cel
Duża rewizja legacy jest najtańsza, kiedy łączy się ją z pracą, którą i tak trzeba wykonać z powodów biznesowych. Przykład: zespół musi zmodyfikować logikę rabatów, bo wchodzi nowa promocja. Zamiast:
- wciskać nową logikę w stary, nieprzetypowany potworek,
- albo przepisywać wszystko na TypeScript tylko z powodu migracji,
można rozbić to na dwa kroki o lepszym ROI:
- Wyciąć fragment odpowiedzialny za rabaty do osobnej funkcji/serwisu (nawet w JS), jednocześnie projektując prostszy interfejs wejścia/wyjścia.
- Nowy fragment napisać już w TS z jasno zdefiniowanymi typami, a stary kod „nakarmić” tymi typami przez wrapper.
Dzięki temu migracja typów jest kosztem marginalnym do i tak planowanej pracy. Biznes dostaje zmianę funkcjonalną, a zespół ma kawałek legacy, który przestał być czarną skrzynką.
Stopnie „kolorowania” kodu typami
Zamiast myśleć: „ten plik jest w TS albo nie”, lepiej przyjąć, że każdy moduł ma poziom nasycenia typami. Praktyczny podział, który łatwo komunikować:
- Poziom 0 – JS bez typów: brak adnotacji, brak
checkJs. Kod działa, ale TypeScript go ignoruje. - Poziom 1 – JS z JSDoc: najprostsze typedefy tylko na eksportach, bez ambicji pełnego pokrycia wnętrza.
- Poziom 2 – TS „na granicach”: plik w
.ts/.tsx, ale dużoanyw środku; zakładasz, że wszystko, co wychodzi na zewnątrz, ma sensowny typ. - Poziom 3 – TS z lokalnymi typami domenowymi: większość istotnych modeli jest opisana, ale część helperów jeszcze używa luźnych typów lub
any. - Poziom 4 – TS „strict”: brak
any(poza wyjątkami), sensowne wykorzystanie unii, typów generycznych, narzędziowych.
Taki prosty „termometr” ułatwia planowanie. Zamiast ogólnego „przetypujemy API”, można konkretnie wpisać do planu: „pakiet @api/orders z poziomu 1 na poziom 3 w dwóch sprintach”. Od razu widać skalę roboty i można to skleić z roadmapą funkcjonalną.
Organizacja pracy i procesów – jak nie zabić zespołu migracją
Minimalny proces „typowy” do ogarnięcia w tydzień
Pełna transformacja sposobu pracy to droga impreza: nowe zasady code review, szkolenia, warsztaty projektowania typów. Na start wystarczy bardzo chudy zestaw reguł, który da realny zysk bez dłubania w organizacji miesiącami:
- Reguła 1: nowe moduły tylko w TS – każdy nowy katalog/feature startuje od
.ts/.tsx. Legacy może zostać w JS, ale „nowy dług” już nie powstaje. - Reguła 2: nie dodajemy nowego
anybez komentarza – jeśli ktoś używaany, musi dopisać krótkie// TODO[types]lub powód. To nie blokuje pracy, ale buduje świadomość. - Reguła 3: każde większe dotknięcie modułu podnosi jego poziom typów o jeden – np. z Poziomu 0 na 1 lub 1 na 2. Nie skaczemy od razu na „strict”, tylko robimy małe kroki.
Tyle wystarczy, by po kilku sprintach kod zaczął realnie zmieniać się w stronę TS, bez formalnych „programów transformacji” i długich prezentacji.
Zmiana nawyków w code review – tanim kosztem
Code review przy migracji może być albo blokadą, albo katalizatorem. Najtańszy sposób, żeby poszło w tę drugą stronę, to kilka prostych zasad ustalonych w zespole:
- Recenzent nie wymusza idealnych typów – jego zadaniem jest złapać rażące
anyna publicznych interfejsach i wyraźne dziury (np. brak typów dla kluczowych modeli), a nie polerować wszystko do perfekcji. - Typy są częścią „Definition of Done” tylko dla nowych rzeczy – dla legacy dopuszczasz kompromisy, byle nie dodawać nowych, niepotrzebnych dziur. Minimalizuje to konflikty z product ownerem.
- Preferowane małe PR-y typowe – łatwiej przepchnąć 50 linii z kilkoma interfejsami niż 1500 linii, gdzie typy mieszają się z refaktoryzacją i zmianami biznesowymi.
Jeżeli zespół ma wrażenie, że każdy PR z typami kończy się dwudniową dyskusją, migracja umrze po pierwszym kwartale. Kiedy kryteria są jasne i skromne, włączanie typów staje się czymś „przy okazji”, a nie osobnym, ciężkim zadaniem.
Oznaczanie „obszarów typowych” w backlogu
Przy dużym repo sama świadomość, które części są już w rozsądnym TS, a które dalej w „dzikim zachodzie” JS-u, ma ogromną wartość organizacyjną. Zamiast utrzymywać osobną tablicę migracji, można:
- dodać prostą etykietę w zadaniach typu
types:touch– każdy ticket, który dotyka modułu, staje się kandydatem na podniesienie jego poziomu typów o jeden; - utrzymywać w dokumentacji technicznej krótką listę pakietów „strategicznych” (np.
@core/auth,@core/payments) z docelowym poziomem typów i orientacyjną datą; - raz na kwartał zrobić szybki przegląd „gdzie jesteśmy z TS-em” – 30 minut, bez slajdów, po prostu mapa obszarów, które zostały już ustabilizowane.
Takie podejście nie wymaga dodatkowego narzędzia ani osobnego projektu w Jirze. Wpinasz migrację w istniejący backlog i dopinasz ją do normalnych zadań produktowych.
Szkolenia i wsparcie – inwestycja z limitem budżetu
Pełne, kilkudniowe szkolenia z zaawansowanego TypeScriptu są kosztowne i nie zawsze potrzebne. Najpierw warto zadbać o tanie, praktyczne formy wsparcia:
- wewnętrzny „cheatsheet” – jedna strona z najczęściej używanymi wzorcami: typowanie propsów, prosty
Result<T>, unie wariantów, obsługaPartial/Pick. To materiał, który większość osób naprawdę czyta; - krótkie sesje live-codingu – 30 minut raz na dwa tygodnie, jedna osoba pokazuje realny PR z migracją kawałka kodu. Zero slajdów, sama praktyka;
- kanał wsparcia (Slack/Teams) – miejsce, gdzie można wrzucić fragment kodu z pytaniem „jak to najlepiej wytipować?” i dostać odpowiedź w ciągu dnia.
Dopiero kiedy baza jest opanowana, ma sens inwestowanie w głębsze tematy: zaawansowane generyki, brandowane typy, modele stanów. Dla większości zespołów 80% zysku przychodzi z bardzo prostych koncepcji – ostrożne unie, typy domenowe, brak przypadkowego any – a nie z fajerwerków typowych.
Ustalanie granic odpowiedzialności – kto „nosi” TypeScript
W dużych organizacjach migracja utknie, jeśli każdy będzie myślał, że to „sprawa architektów”. Z drugiej strony zrzucenie całej odpowiedzialności na pojedynczą osobę też nie działa. Rozsądny, niskobudżetowy podział ról wygląda tak:
- „owner” konfiguracji – jedna osoba (lub mały zespół) odpowiada za
tsconfig, integrację z bundlerem i pipeline’m CI. Unikamy sytuacji, gdzie każdy podkręca flagi typów po swojemu. - lokalni „mistrzowie typów” – w każdym zespole projektowym jedna osoba, do której można pójść z pytaniami. Nie musi być ekspertem od TypeScriptu, wystarczy, że o krok wyprzedza resztę.
- wszyscy deweloperzy – odpowiadają za typy w kodzie, który dotykają. Nie ma kast, które „mogą” pisać TS i takich, które nie mogą; jest po prostu różny poziom biegłości.
Taki układ nie wymaga tworzenia nowych formalnych stanowisk, ale daje jasność, kto decyduje o globalnych zmianach, a kto pomaga na poziomie zespołów. Mniej chaosu, mniej dyskusji „kto miał to ogarnąć”.
Planowanie migracji w sprintach – jak to policzyć
Migracja typów konkuruje z normalnym rozwojem produktu o ten sam czas. Żeby nie kończyło się to wiecznym „zrobimy to kiedyś”, przydaje się prosty model alokacji czasu:
- 2–5% capacity na typy w każdym sprincie – dla zespołu robiącego 40 story pointów oznacza to 1–2 punkty na zadania stricte typowe (np. „przenieść
shared/utils/datena poziom 3”). - Do 20% typów w zadaniach produktowych – każda większa karta rozwojowa może mieć fragment pracy „typowej” w scope (np. dopisanie typów do nowych endpointów API).
Takie liczby są łatwe do przełknięcia dla product ownera – to nie jest „poświęcamy sprint na czysty tech debt”, tylko mała premia jakościowa dokładana do normalnych zadań. Po kilku miesiącach różnica w jakości kodu i łatwości zmian staje się już zauważalna bez spektakularnych, kosztownych inicjatyw.
Najczęściej zadawane pytania (FAQ)
Czy w dużym projekcie naprawdę opłaca się przejść z JavaScript na TypeScript?
W dużych repozytoriach TypeScript zwykle się „spłaca”, bo zmniejsza liczbę regresji i przyspiesza refaktoryzacje. Kompilator łapie całe klasy błędów, które w czystym JS wychodzą dopiero na produkcji, a typy robią za żywą dokumentację dla nowych osób w zespole. Nawet kilkunastoprocentowe skrócenie czasu zmian na większych feature’ach potrafi przełożyć się na realne oszczędności w sprintach.
Nie chodzi tylko o „ładniejsze typy”, ale o kontrolę ryzyka w długowiecznym produkcie: stabilniejsze zmiany, mniej chaosu na granicach modułów, lepsze wsparcie IDE. Jeśli projekt ma żyć latami, często się zmienia i rozwija go więcej niż kilka osób, ROI z migracji jest zazwyczaj wysokie.
Kiedy lepiej zostać przy JavaScript + JSDoc + ESLint zamiast migrować do TypeScript?
Są sytuacje, w których pełna migracja do TS to przerost formy nad treścią. Dotyczy to zwłaszcza krótkotrwałych projektów (kampanie, proste landingi), gdzie kod raczej nie będzie rozwijany latami. Podobnie, jeśli projekt nie ma sensownych testów, ESLint jest wyłączony, a styl kodu jest losowy, dorzucenie TypeScriptu tylko pogłębi bałagan i obciąży zespół.
W takich przypadkach rozsądniejszy jest „wariant budżetowy”: dobre reguły ESLint, konsekwentne JSDoc, włączony checkJs. Taki zestaw daje autouzupełnianie, częściową kontrolę typów i większy porządek w kodzie, bez kosztu pełnej migracji i ostrej krzywej nauki dla całego zespołu.
Jak ocenić, czy nasze repozytorium i zespół są gotowe na migrację do TypeScript?
Najprostszy test dojrzałości to kilka konkretnych pytań. Po pierwsze: czy są testy (choćby częściowe) na kluczowych ścieżkach biznesowych? Migracja bez testów to hazard, bo trudno odróżnić błąd w typach od zepsanej logiki. Po drugie: czy obowiązują spójne standardy kodu (ESLint, Prettier), czy każdy pisze po swojemu? TypeScript dorzuca kolejną warstwę reguł, więc potrzebny jest minimalny porządek startowy.
Po trzecie: jak wygląda rotacja ludzi i decyzyjność? Jeśli co chwilę zmienia się skład, a nikt technicznie nie trzyma kierunku, migracja rozjedzie się na kilka stylów typowania i porzucone podejścia. W takiej sytuacji lepiej najpierw ustabilizować procesy (linting, testy na newralgicznych modułach, jedna techniczna „osoba od decyzji”), a dopiero potem ruszać z TS.
Od czego zacząć migrację z JavaScript na TypeScript w dużym monorepo?
Najtaniej i najbezpieczniej zacząć od audytu, a nie od masowego przepinania plików na .ts. Na start warto zmapować: typ repo (monorepo czy wiele mniejszych), liczbę pakietów i to, które z nich są aktywnie rozwijane, a które leżą w kącie jako „legacy”. Prosty rzut oka na katalogi legacy/, old/, v1/, shared/ zwykle pokazuje, gdzie jest największy dług i gdzie zmiana będzie najbardziej bolesna.
Dobrym „pierwszym celem” są współdzielone utilsy i modele w katalogach w stylu shared/ czy common/, ale tylko wtedy, gdy są sensownie używane i mają testy. Lepiej migrować małe, dobrze rozumiane wycinki kodu i na nich wypracować standardy, niż brać na klatę cały legacy na raz. To zmniejsza ryzyko zablokowania roadmapy biznesowej.
Jakie są typowe problemy i pułapki przy migracji dużego repozytorium do TypeScript?
Najczęstsza pułapka to próba „big bang” – jednorazowego przepisania wszystkiego. Kończy się to setkami błędów typów, zablokowanymi feature’ami i frustracją zespołu. Drugi klasyk to nadmierny perfekcjonizm: wszędzie strict, zero any, rozbudowane typy generowane, zanim zespół w ogóle czuje TS. W praktyce potrzeba kompromisu: miejscami świadomie używa się any lub prostszych typów, żeby nie spalić sprintów na polerowanie definicji, które zaraz i tak się zmienią.
Problemem bywa też ignorowanie dynamicznych fragmentów kodu, np. masowego użycia obj[key] z kluczem z runtime czy nieudokumentowanych odpowiedzi z zewnętrznych API. W takich miejscach koszt dokładnego typowania jest wysoki. Często taniej jest najpierw ucywilizować te fragmenty w samym JS (np. wprowadzić warstwę adapterów), a dopiero później typować tę warstwę, zamiast próbować opisać cały chaos typami.
Jak TypeScript wpływa na czas developmentu i onboarding nowych programistów?
Na początku development potrafi zwolnić: zespół uczy się TS, konfiguracji, nowych błędów kompilatora. Ten koszt jest najbardziej odczuwalny w pierwszych tygodniach migracji, zwłaszcza jeśli nikt wcześniej nie pracował z TS lub konfiguracja jest przesadnie ambitna. Dlatego na start lepiej trzymać się prostych reguł i nie włączać od razu wszystkich możliwych opcji „strict”.
Po fazie wejścia zyski zaczynają przeważać. Nowa osoba w projekcie szybciej rozumie kontrakty między modułami, bo widzi typy i podpowiedzi IDE zamiast polować po plikach. Przy refaktoryzacjach kompilator prowadzi za rękę przez wszystkie miejsca wymagające aktualizacji, zamiast liczyć wyłącznie na wyszukiwanie tekstowe i ręczne testy. W dłuższej perspektywie skraca to czas wdrożenia i ogranicza „niewidoczny” koszt ciągłych poprawek po regresjach.
Czy trzeba od razu używać TypeScript do transpilacji, czy można tylko do typów?
W wielu dużych projektach tańszą ścieżką na start jest używanie TypeScript głównie do sprawdzania typów, przy zostawieniu obecnego toolchainu do transpilacji (np. Babel, esbuild). Umożliwia to stopniowe wprowadzenie TS bez wywracania całego procesu buildów, testów i CI/CD w jednym kroku. W praktyce oznacza to konfigurację tsconfig pod kątem typów i integrację z istniejącym bundlerem/test runnerem.
Pełne przejście na tsc do transpilacji można odłożyć na później albo w ogóle z niego zrezygnować, jeśli obecne narzędzia dobrze działają. Takie podejście zmniejsza ryzyko techniczne i rozkłada koszt zmian w czasie, co jest wygodne przy napiętej roadmapie produktowej.






