JSON do TypeScript: interfejsy, strażnicy typów i granice generowania
Podsumowanie: Wygenerowanie interfejsu z przykładowego JSON-a to połowa pracy: pola opcjonalne, null, unie, daty i duże liczby całkowite wymagają ręcznego przejścia, a typy nie walidują w czasie wykonania.

Najważniejsze: wygeneruj interfejs, aby zaoszczędzić pisania, a potem ponownie sprawdź opcjonalność i null, zachowaj daty oraz duże liczby całkowite jako łańcuchy i dodaj walidację w czasie wykonania na granicy; traktowanie wygenerowanych typów jako kontraktu nie daje żadnej walidacji.
Generator widzi tylko jedną próbkę, którą mu wkleisz. Czy pola mogą być nieobecne, czy wartości mogą być null i jaki format mają daty — wszystko to zgadywanie, a złe domysły musisz poprawić sam.
1. Co generator naprawdę robi
Wklej próbkę JSON do JSON do TypeScript, a otrzymany interfejs będzie streszczeniem kształtu: policzy klucze i typy wartości, które napotkał, a następnie wybierze najwęższe dostępne przedstawienie.
{
"id": 1,
"email": "ada@example.com",
"profile": { "nickname": "Ada", "age": 36 },
"tags": ["admin", "beta"],
"createdAt": "2026-10-31T08:00:00Z"
}
Wyjście wygląda mniej więcej tak:
interface User {
id: number;
email: string;
profile: { nickname: string; age: number };
tags: string[];
createdAt: string;
}
To się kompiluje, ale opisuje idealną wartość, a nie rzeczywisty kontrakt punktu końcowego. Generator nie wie, czy email może być nieobecne, czy tags może być puste ani czy createdAt zawsze niesie strefę czasową. Nie wie nawet, że id w innych rekordach może być łańcuchem.
Dlatego właściwe oczekiwanie brzmi: generator oszczędza pisanie, człowiek dostarcza znaczenie.
2. Pola opcjonalne i null: ? to nie | null
To część wygenerowanego wyniku, która jest najczęściej kopiowana i najczęściej się psuje.
| Forma TypeScript | Znaczenie | Pasujący JSON |
|---|---|---|
email: string |
Zawsze obecne, zawsze łańcuch | {"email":"a@b.c"} |
email?: string |
Klucz może być nieobecny; gdy jest, nie może być null | {} lub {"email":"a@b.c"} |
email: string | null |
Klucz jest zawsze obecny, wartość może być null | {"email":null} |
email?: string | null |
Klucz może być nieobecny, a wartość może być null | Wszystkie trzy przypadki |
Generator widział jedną próbkę, więc zwykle wypisuje pierwszą formę. Jeśli API faktycznie może zwrócić null, typ kłamie: wszystko się kompiluje, a potem email.toLowerCase() rzuca w czasie wykonania.
Są dwa sposoby naprawy:
- Zbierz kilka prawdziwych próbek obejmujących zarówno „brakujący klucz”, jak i „wartość is null”, a następnie pozwól generatorowi wywnioskować ponownie.
- Po wygenerowaniu zmień pola ręcznie na
?: T | nulli faktycznie obsłuż obie gałęzie w kodzie.
Nie wybieraj skrótu polegającego na oznaczeniu wszystkiego ?: string. Opcjonalne znaczy „klucz może nie istnieć”, a nie „wartość może być pusta”. undefined i null zachowują się też inaczej w JSON.stringify: pierwsze jest pomijane, drugie jest zachowywane jako null.
3. Nierówne elementy tablic i unie typów
Tablice to struktura najłatwiejsza do zepsucia. Jeśli tags występuje jako ["admin"], [] i ["beta", 3] w trzech rekordach, generator może wypisać string[], (string | number)[] albo unknown[] w zależności od próbek, które widział.
Człowiek musi zdecydować:
- Typy elementów naprawdę są mieszane: zapisz
(string | number)[]i zawężaj w miejscu użycia. - Tylko jeden rekord jest brudny: popraw dane. Nie ukrywaj go przez
any[]. - Puste tablice: generator nie potrafi wywnioskować typu elementu i cofa się do
never[]lubany[], więc musisz go uzupełnić. - Krotki o stałej długości: użyj
[string, string]tylko wtedy, gdy biznes naprawdę gwarantuje długość; inaczej dodanie elementu później staje się błędem kompilacji.
Tablice obiektów wymagają uwagi również przy nierównych kluczach:
interface OrderItem {
sku: string;
quantity: number;
discount?: number; // some records do not carry this field
}
Opcjonalność umieszczaj na typie elementu, a nie na tablicy. items?: OrderItem[] znaczy, że sama tablica może być nieobecna, co jest zupełnie innym stwierdzeniem.
4. Daty, duże liczby całkowite, wyliczenia: typy, których JSON nie ma
JSON ma tylko łańcuchy i liczby; znaczenie biznesowe żyje w konwencjach. Generator wnioskuje z literałów, więc popraw je ręcznie:
| Literał JSON | Typowe wyjście generatora | Co powinno być |
|---|---|---|
"2026-10-31T08:00:00Z" |
string |
Zachowaj string w warstwie transportu i konwertuj w miejscu użycia; nigdy nie deklaruj Date bezpośrednio |
1717075200000 |
number |
Użyj number dla znacznika czasu w milisekundach i udokumentuj jednostkę |
"1234567890123456789" |
string |
Zachowaj jako string, aby number nie utracił precyzji |
1 i "1" mieszane |
number | string |
Znormalizuj do jednej formy; nie wciskaj unii do kodu biznesowego |
"admin" |
string |
Unia literałów 'admin' | 'editor' albo wspólna stała wyliczeniowa |
true i "true" mieszane |
boolean | string |
Popraw źródło danych, zamiast generować ten typ |
Najważniejsza zasada: daty w JSON-ie są zawsze łańcuchami. Zapis createdAt: Date to częsty samobójczy kompromis — JSON.parse nigdy nie tworzy Date, więc wartość w czasie wykonania nadal jest łańcuchem i .getTime() rzuca. Zachowaj typ transportowy jako string, jawnie wywołaj new Date(value) na granicy i sprawdź Number.isNaN.
Duże liczby całkowite działają tak samo. Gdy id przekracza 2^53, JSON.parse utracił już precyzję, a wygenerowanie number tylko cementuje błąd w typie. Backend zwraca łańcuch, frontend zachowuje go jako łańcuch; to jedyna niezawodna opcja.
5. Wygenerowane typy nie walidują w czasie wykonania
Typy TypeScript są wymazywane w czasie kompilacji. Niezależnie od precyzji interfejsu nie potrafi zatrzymać odpowiedzi {"id": "x"} — ogranicza twój własny kod, a nie bajty nadchodzące przez sieć.
/**
* Check whether a runtime value is a user object.
* @param value The runtime value to validate
* @returns Whether it satisfies the minimum shape of User
*/
function isUser(value: unknown): value is User {
if (typeof value !== 'object' || value === null) {
return false;
}
const candidate = value as Record<string, unknown>;
return typeof candidate.id === 'number' && typeof candidate.email === 'string';
}
Strażnik typu obejmuje tylko pola, które wpiszesz do sprawdzenia. Wraz z mnożeniem pól ręcznie pisane strażniki stają się drugą definicją do utrzymania, która dryfuje od interfejsu.
Praktyczny kompromis wygląda tak:
| Podejście | Walidacja w czasie wykonania | Źródło typów | Pasuje do |
|---|---|---|---|
| Tylko interfejs | Brak | Ręczne lub wygenerowane | Zaufane dane wewnętrzne, ograniczenia czasu kompilacji |
| Interfejs plus ręczny strażnik | Tak | Dwie definicje, które dryfują | Kilka pól, jasna granica |
| zod / valibot | Tak | Typy wnioskowane ze schematu | Zewnętrzne API, formularze, konfiguracja |
| JSON Schema plus walidator | Tak | Typy wygenerowane ze schematu | Kontrakty między językami, dokumentacja |
Jeśli dane przychodzą z zewnątrz, uczyń schemat jedynym źródłem prawdy i wyprowadź typy przez z.infer lub json-schema-to-ts. Wtedy „walidacja przechodzi” i „typ jest poprawny” to ten sam fakt, a drugiej definicji do dryfowania nie ma. Pełne podejście do dwukierunkowej synchronizacji opisano w Synchronizacja schematu i typów TypeScript.
6. Interface czy type: nie kłóć się o wygenerowany wynik
Generatory zwykle wypisują interface, ale w większości sytuacji oba są wymienne. Naprawdę znaczą trzy możliwości:
- Łączenie deklaracji:
interfacemożna zadeklarować wielokrotnie, a on scala się automatycznie;typenie potrafi. Pomaga to przy łataniu typów zewnętrznych, ale modele biznesowe powinny go unikać — scalanie pozwala „dodatkowym polom” przejść przez kompilator niezauważenie. - Unie i typy odwzorowane:
typepotrafi bezpośrednio wyrazićA | B,Partial<T>i typy literałów szablonowych;interfacenie potrafi. - Rekurencja: oba ją wspierają, ale złożone kształty rekurencyjne czytają się naturalniej jako
type.
Bardziej użyteczna zasada zespołowa: używaj interface dla kształtów obiektów, a type dla unii, typów pomocniczych i typów funkcji. Zostaw wygenerowany wynik w domyślnej formie i przeznacz czas przeglądu na opcjonalność oraz walidację w czasie wykonania.
7. Łańcuch narzędzi: trzy sposoby generowania
| Podejście | Wejście | Mocna strona | Ograniczenie |
|---|---|---|---|
| JSON do TypeScript w tej witrynie | Próbka JSON | Działa od razu, przetwarza lokalnie | Bazuje tylko na aktualnej próbce; wymaga ręcznego zawężania |
| quicktype | JSON / JSON Schema | Wyjście w wielu językach, integracja z CLI | Zachowawcze wnioskowanie, unie mogą być zbyt szerokie |
| json-schema-to-typescript | JSON Schema | Napędzane kontraktem i powtarzalne | Wymaga najpierw wiarygodnego schematu |
Typowe wywołanie quicktype:
npx quicktype -s json -o user.ts user.json --just-types
--just-types wypisuje tylko typy bez kodu konwersji w czasie wykonania, co pasuje do projektów mających już warstwę parsowania. Jeśli schemat jest ręcznie pisany lub produkowany przez backend, json-schema-to-typescript pasuje lepiej, ponieważ respektuje required, enum i additionalProperties — ograniczenia, których wnioskowanie z próbki zwyczajnie nie widzi.
Generowanie schematu ma własne pułapki, takie jak oznaczenie każdego pola jako wymaganego albo ciche zamienienie null w opcjonalność; zobacz typowe pułapki generatora JSON Schema.
8. Lista kontrolna przed commitem wygenerowanych typów
Przejdź te pięć punktów, zanim zatwierdzisz wynik:
- Znormalizuj nazwy pól: nie pozwól, by
userIdiuser_idwspółistniały; zamapuj raz w warstwie konwersji. - Ponownie sprawdź opcjonalność: porównaj z prawdziwymi odpowiedziami i znajdź, które pola mogą być nieobecne lub
null. - Zachowaj daty i identyfikatory jako łańcuchy: udokumentuj jednostkę i format w typie i parsuj jawnie w miejscu użycia.
- Zredukuj
anyiunknown: gdziekolwiek generator wypisałany, albo dodaj typ, albo zachowajunknowni dodaj strażnika. - Dodaj walidację w czasie wykonania: zwaliduj dane zewnętrzne co najmniej raz; nie oczekuj, że interfejs złapie złe dane.
Jeśli projekt używa kilku bibliotek JSON (JSON.parse, superjson, lossless-json), ich zachowanie dla dużych liczb całkowitych i dat się różni, więc potwierdź to podczas wyboru. Zobacz Wybór biblioteki do parsowania JSON.
Dlaczego generator oznacza każde pole opcjonalne jako wymagane?
Widział jedną próbkę, więc każdy klucz, który się pojawił, traktuje jako zawsze obecny. Dostarcz wiele próbek obejmujących różne przypadki albo generuj z JSON Schema — tamtejsza tablica required jest źródłem autorytatywnym.
Dlaczego zadeklarowanie Date w interfejsie zawodzi w czasie wykonania?
JSON.parse produkuje tylko łańcuchy, liczby, wartości logiczne, null, obiekty i tablice; nigdy nie tworzy Date. Zadeklarowanie Date to obietnica czasu kompilacji, podczas gdy wartość w czasie wykonania pozostaje łańcuchem. Zachowaj string w warstwie transportu i konwertuj na granicy.
Czy wygenerowane typy można używać jako walidacji?
Nie. Typy TypeScript są wymazywane po kompilacji i nie narzucają niczego danym w czasie wykonania. Waliduj dane zewnętrzne przez zod, valibot, ajv albo ręcznie napisanego strażnika typu.
Czy duży identyfikator całkowity typować jako number czy string?
Jeśli może przekroczyć 2^53, musi być string. JSON.parse traci precyzję podczas parsowania, a typowanie jako number tylko wpisuje błąd w typ, gdzie nie da się go już naprawić.