Published on

The Power of Typescript Satisfied Operator in Less than 2 Minutes

Authors

By default, TypeScript tries to recognize the type of a variable by itself, meaning there is always some kind of type, no matter what we do.

const user = {
    id: 1,
    email: 'example@mail.com',
};

typeof user = {
    id: number // generated automatically
    email: string // generated automatically
};

Already existing keys can only be changed to a value that matches the previously declared type. In this case, email was a string, so we can only change it to a string.

This also means the object will always maintain the same, expected shape.

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; }'

In real projects, relying only on TypeScript's assumption of types might not always work as we might find ourselves in situations where we don't want to allow all types.

For this reason, we can use variable annotation.

// variable annotation
const user: Record<string, string | number | boolean> = {
  id: 1,
  email: 'test@mail.com',
}

This method works on the variable level, which means we change the way TypeScript sees the type of the variable. It no longer tries to recognize it by itself.

In this case, it doesn't know we have an email key, it only knows there should be a string as a key and the value is a string, number, or 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

It works, but without TypeScript's assumption, we are able to create keys that never existed as well as read keys, which we shouldn't read as long as we follow the type.

It's a behavior that has very rare use cases as it ruins the whole idea behind type-safe writing of code.

We should tend to keep TypeScript's assumption working, but we couldn't really do anything about it... until we got the satisfies operator.

The satisfies operator, compared to previous methods, works on values, not variables, which means it checks if the current type matches (satisfies) the selected type, leaving type assumption to TypeScript.

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'

It allows TypeScript to assume that the declared object has the expected type and contains only id as number and email as a string.

The satisfies operator provides a check without having any impact on the type.

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`

Yet the true power of satisfies comes in connection with as const, where the lack of overwriting types allows TypeScript to show the developer exact values.

So remember my dear dev, satisfies checks the type of an object, without having impact on the TypeScript's assumption and should be one of your main ally again typing constant objects.