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 symbol
`unique symbol` daje 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 UserId
Jedno 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.