- Opublikowano
Moc operatora satisfies w TypeScripcie w mniej niż 2 minuty
- Autorzy
Domyślnie TypeScript sam próbuje rozpoznać typ zmiennej — co oznacza, że jakiś typ istnieje zawsze, cokolwiek zrobimy.
const user = {
id: 1,
email: 'example@mail.com',
};
typeof user = {
id: number // generated automatically
email: string // generated automatically
};
Istniejące już klucze można nadpisać tylko wartością zgodną z wcześniej zadeklarowanym typem. Tutaj email był stringiem, więc możemy podstawić pod niego wyłącznie stringa.
Oznacza to również, że obiekt zawsze zachowa ten sam, oczekiwany kształt.
const user = {
id: 1, // Read as number
email: 'test@mail.com', // Read as string
}
user.email = 'example@email.com'
user.email = true // ERROR: Type 'boolean' is not assignable to type 'string'
user.notExisting = 'Test' // ERROR: Property 'notExisting' does not exist on type '{ id: number; email: string; }'
W realnych projektach poleganie wyłącznie na tym, co TypeScript sam sobie wywnioskuje, nie zawsze wystarcza — trafiamy na sytuacje, w których nie chcemy dopuszczać wszystkich typów.
Z tego powodu możemy sięgnąć po adnotację zmiennej.
// variable annotation
const user: Record<string, string | number | boolean> = {
id: 1,
email: 'test@mail.com',
}
Ta metoda działa na poziomie zmiennej, czyli zmieniamy sposób, w jaki TypeScript widzi jej typ. Przestaje go rozpoznawać samodzielnie.
W tym przypadku nie wie, że mamy klucz email — wie tylko tyle, że kluczem ma być string, a wartością string, number albo boolean.
const user: Record<string, string | number | boolean> = {
id: 1,
email: 'test@mail.com',
}
user.email = 'example@.com'
user.email = true
user.notCorrectValue = {} // ERROR: Type '{}' is not assignable to type 'string | number | boolean'
console.log(user.neverExisting) // <- We can read a value that never exists
Działa, ale bez wnioskowania TypeScriptu jesteśmy w stanie tworzyć klucze, których nigdy nie było, oraz odczytywać klucze, których — trzymając się typu — odczytywać nie powinniśmy.
To zachowanie ma bardzo rzadkie zastosowania, bo niszczy całą ideę pisania kodu type-safe.
Powinniśmy dążyć do tego, żeby wnioskowanie TypeScriptu dalej działało, ale za bardzo nie dało się z tym nic zrobić... dopóki nie dostaliśmy operatora satisfies.
Operator satisfies, w odróżnieniu od poprzednich metod, działa na wartościach, a nie na zmiennych — sprawdza, czy bieżący typ pasuje do (spełnia) wybranego typu, zostawiając wnioskowanie typu TypeScriptowi.
const user = {
id: 1,
email: 'asdsadas',
} satisfies Record<string, string | number | boolean>
user.id = 'string Id' // ERROR: Type 'string' is not assignable to type 'number'
user.email = 'example@.com'
user.test = true // ERROR: Property 'test' does not exist on type '{ id: number; email: string; }'
user.email = true // ERROR: Type 'boolean' is not assignable to type 'string'
Pozwala to TypeScriptowi przyjąć, że zadeklarowany obiekt ma oczekiwany typ i zawiera wyłącznie id jako number oraz email jako string.
Operator satisfies daje sprawdzenie bez żadnego wpływu na sam typ.
type User = Record<string, string | number | boolean>
const USER_: User = {
email: '1',
} as const
USER_.email // <- `email` is shown as `string | number | boolean`
const USER = {
email: '1',
} as const satisfies User
USER.email // <- `email` is shown as `1`
Prawdziwa moc satisfies wychodzi jednak dopiero w połączeniu z as const — brak nadpisywania typów pozwala TypeScriptowi pokazać deweloperowi dokładne wartości.
Zapamiętaj więc, drogi devie: satisfies sprawdza typ obiektu, nie wpływając na wnioskowanie TypeScriptu i powinien być jednym z twoich głównych sprzymierzeńców przy typowaniu stałych obiektów.