Wzorce
Jak powstrzymać przekazanie UserId tam, gdzie oczekiwano OrderId, skoro oba są zwykłymi tekstami?
TypeScript jest strukturalny, więc dwa aliasy string to ten sam typ. Fantomowa właściwość czyni je różnymi, nie zmieniając wartości.
Przepis
declare const brand: unique symbol type Brand<T, Name extends string> = T & { readonly [brand]: Name } type UserId = Brand<string, 'UserId'> type OrderId = Brand<string, 'OrderId'> const asUserId = (raw: string): UserId => raw as UserId declare function findUser(id: UserId): void const id = asUserId('u_1') findUser(id)
Build to kompiluje i sprawdza każdy wynik poniżej.
Jak to działa
- 01
declare const brand: unique symbolunique symboldaje klucz, którego nic innego nie wytworzy. Klucz tekstowy pozwoliłby przypadkowemu obiektowi spełnić stempel. - 02
type Brand<T, Name extends string> = T & { readonly [brand]: Name }Przecięcie dokłada właściwość istniejącą wyłącznie w typie. W czasie wykonania wartość to wciąż dokładnie ten tekst, od którego zacząłeś.
- 03
const asUserId = (raw: string): UserId => raw as UserIdJedno rzutowanie w jednym miejscu to cena. Wszędzie indziej kompilator egzekwuje rozróżnienie za darmo — i dlatego ta funkcja powinna być jedynym wejściem.
Co dostajesz
UserId extends string ? true : false→truestring extends UserId ? true : false→falseTa asymetria jest całą funkcją: ostemplowane id wciąż jest tekstem, ale goły tekst nie jest ostemplowanym id.
UserId extends OrderId ? true : false→false
Gdzie to zawodzi
Stempel jest wymazywany, więc JSON.parse bez oporu poda ci coś o typie UserId, co nigdy nie przeszło przez twój konstruktor. Stemple chronią twój kod przed nim samym; nie walidują danych przekraczających granicę.
Wniosek
Sięgaj po stempel, gdy dwie wartości dzielą reprezentację, a nie mogą dzielić znaczenia. Trzymaj dokładnie jedną funkcję produkującą każdą z nich.

