programowanieArchitekt TypeScript

Jakie są różnice między typowaniem strukturalnym a nominalnym w TypeScript? Czy można zrealizować typowanie nominalne, a jeśli tak, to jak? Jakie problemy może to rozwiązać?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź

TypeScript wykorzystuje typowanie strukturalne (structural typing), znane również jako "typowanie na podstawie kaczki". Dla zgodności typów ważna jest struktura (sygnatura), a nie nazwa czy pochodzenie typu.

Przykład:

interface Point2D { x: number; y: number; } interface Coord2D { x: number; y: number; } // Te typy są zamienne: Point2D i Coord2D, ponieważ struktura jest taka sama. const foo: Point2D = { x: 1, y: 2 }; const bar: Coord2D = foo; // OK!

Typowanie nominalne (nominal typing): dla zgodności typów ważna jest "nazwa" lub "fabryka", struktura nie jest ważna.

W TypeScript typowanie nominalne nie jest natywnie wspierane, ale można je emulować przy użyciu typów oznaczonych (branded types):

type USD = number & { readonly __brand: unique symbol } type EUR = number & { readonly __brand: unique symbol } let priceUSD: USD; let priceEUR: EUR; // priceUSD = priceEUR; // Błąd! Różne marki.

Po co to stosować? Na przykład, aby rozróżniać typy, które mają tę samą strukturę, ale mają różne znaczenia — waluty, userID/tokenID, wielkości fizyczne itp.


Pytanie z zasadzka

Pytanie: Dlaczego poniższy kod kompiluje się bez błędów, chociaż Address i UserId to logicznie różne typy?

interface Address { value: string; } interface UserId { value: string; } let id: UserId = { value: "test" }; let addr: Address = id; // OK

Odpowiedź: Ponieważ w TypeScript ważna jest struktura, a nie nazwa typu. Oba typy to po prostu "obiekt z value: string".


Przykłady rzeczywistych błędów spowodowanych brakiem znajomości niuansów tematu


Historia

Projekt: System finansowy z transakcjami w USD/EUR. Kwoty przekazywano jako number. Pewnego razu pomylono waluty podczas dodawania — przez typowanie strukturalne TypeScript tego nie wykrył. Później wprowadzono typy oznaczone, aby wyeliminować takie błędy na etapie kompilacji.


Historia

Projekt: W rozwijanym REST API używano obiektów dla identyfikatorów różnych bytów (userId, groupId), oba z polem value: string. Z powodu błędu userId był podstawiany zamiast groupId, a błąd wykrywała jedynie logika biznesowa na serwerze.


Historia

Projekt: W bibliotece parserów dla DSL używano tych samych struktur (type value = { kind: 'num'|'str', value: number|string }). Jednorodne struktury pomieszały się między różnymi częściami kodu, co prowadziło do błędów logicznych. Dodano sztuczne pola brand do rozdzielenia.