JSON Toolkit

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.

Średniozaawansowany~9 min czytaniaOpublikowano 2026-10-31Zaktualizowano 2026-11-04
Okładka przewodnika po interfejsach JSON do TypeScript i strażnikach typów

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:

  1. 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.
  2. Po wygenerowaniu zmień pola ręcznie na ?: T | null i 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[] lub any[], 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: interface można zadeklarować wielokrotnie, a on scala się automatycznie; type nie 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: type potrafi bezpośrednio wyrazić A | B, Partial<T> i typy literałów szablonowych; interface nie 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:

  1. Znormalizuj nazwy pól: nie pozwól, by userId i user_id współistniały; zamapuj raz w warstwie konwersji.
  2. Ponownie sprawdź opcjonalność: porównaj z prawdziwymi odpowiedziami i znajdź, które pola mogą być nieobecne lub null.
  3. Zachowaj daty i identyfikatory jako łańcuchy: udokumentuj jednostkę i format w typie i parsuj jawnie w miejscu użycia.
  4. Zredukuj any i unknown: gdziekolwiek generator wypisał any, albo dodaj typ, albo zachowaj unknown i dodaj strażnika.
  5. 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ć.