Kamil Owczarek
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.