TypeScript satisfies: wąskie literały bez poszerzania typu
Adnotacja typu poszerza literały obiektu. satisfies sprawdza kształt i zostawia wąską inferencję, więc autocomplete i narrowing dalej działają.
Dopisujesz adnotację do obiektu konfiguracji „dla bezpieczeństwa” i nagle palette.green.toUpperCase() wywala się w checkerze. W runtime to nadal string. TypeScript po prostu o tym zapomniał.
Klasyczna pułapka: adnotacja zastępuje typ wywnioskowany i poszerza literały. satisfies sprawdza kształt i zostawia wąską inferencję, więc autocomplete i narrowing dalej działają. Oficjalny opis: TypeScript 4.9 — The satisfies operator.
Problem: adnotacja zjada literały
Mapujesz identyfikatory tras na loadery w aplikacji React / Next.js (przykład):
type Loader = () => Promise<{ title: string }>;
const loaders: Record<RouteId, Loader> = {
home: async () => ({ title: "Home" }),
blog: async () => ({ title: "Blog" }),
about: async () => ({ title: "About" }),
};
// OK... aż zechcesz listę kluczy jako tuple literałów:
const routeIds = Object.keys(loaders);
// string[] - nie ("home" | "blog" | "about")[]Albo klasyczny przykład z paletą z dokumentacji. Adnotacja Record łapie literówkę bleu, ale palette.green staje się string | RGB i metody stringa znikają.
Rozwiązanie: satisfies zamiast poszerzania
Użyj satisfies, gdy zależy Ci na precyzyjnym typie wartości i jednocześnie na sprawdzeniu kształtu:
type RGB = [red: number, green: number, blue: number];
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Record<Colors, string | RGB>;
// Nadal string - metody działają
const green = palette.green.toUpperCase();
// Nadal tuple - indeks zostaje precyzyjny
const r = palette.red[0];Co się zmienia:
- Brakujące albo źle nazwane klucze wywalają check (jak przy adnotacji
Record). - Typy właściwości zostają konkretne (
"#00ff00"zostaje stringiem, niestring | RGB). - Masz poręcz bez wyrzucania inferencji.
Dla mapy tras ten sam wzorzec:
const loaders = {
home: async () => ({ title: "Home" }),
blog: async () => ({ title: "Blog" }),
about: async () => ({ title: "About" }),
} satisfies Record<RouteId, () => Promise<{ title: string }>>;
type KnownRoute = keyof typeof loaders; // "home" | "blog" | "about"Opcjonalnie, gdy chcesz też readonly literały: as const satisfies SomeType (kolejność ma znaczenie: najpierw as const, potem satisfies). Ten układ często pojawia się przy configach i mapach i18n; warto też zajrzeć do Total TypeScript o satisfies.
Pułapki i ograniczenia
- Bierz
satisfiesdla literałów obiektów, które potem czytasz właściwość po właściwości (configi, tabele tras, flagi). - Zostaw zwykłą adnotację, gdy chcesz typ poszerzony (np. mutowalny
Record, do którego później przypisujesz). satisfiesdziała tylko w compile-time. Nie ma go w runtime i nie waliduje payloadów z API.- Wymaga TypeScript 4.9+. Na starszych projektach najpierw podnieś
typescript(i wersję TS w edytorze). - Nie zamraża obiektu i nic nie kopiuje. Mutowalność zostaje, dopóki nie dodasz
as const/readonly. - Excess property checks nadal działają jak dla literałów; zagnieżdżone obiekty czasem potrzebują własnego
satisfies. - Zbyt luźne ograniczenie po prawej (
unknown, szerokie unie) daje mało korzyści. Doprecyzuj kształt, który naprawdę masz na myśli.
Gdy mid-level frontend mówi „otypowałem obiekt i straciłem autocomplete”, zwykle winne jest poszerzenie przez adnotację.
satisfies zostawia check i wąski typ. Użyj tego przy kolejnej mapie konfiguracji zamiast kolejnej adnotacji „dla bezpieczeństwa”.