AcademyTypeScript
Type assertions: as vs satisfies
What a type assertion actually does to your program, the two places it is defensible, and why satisfies gives you the check without losing the type.
3 min readUpdated 3 exercises
as does not convert anything
A type assertion changes what the compiler believes. It does not touch the value, run a check, or produce any output:
const value = '42' as unknown as number;
value.toFixed(2); // compiles
// TypeError: value.toFixed is not a functionThat is the whole feature. as is you telling the compiler you know better, and from that line on it stops verifying the thing you claimed. Every runtime error that begins "but TypeScript said it was..." starts at an assertion someone wrote to make an error go away.
The non-null assertion is the same promise in one character:
const el = document.getElementById('app')!;
el.classList.add('ready'); // TypeError if the element is not thereWhere it is defensible
Two places, and they have something in common — you know something the type system cannot:
// 1. Immediately after a check the compiler cannot follow.
if (!isUser(payload)) throw new Error('bad payload');
const user = payload as User; // better: make isUser a type predicate
// 2. Minting a branded type, in the one function allowed to.
function toUserId(raw: string): UserId {
if (!/^u_/.test(raw)) throw new Error('not a user id');
return raw as UserId; // validated one line above
}Both have the validation next to the assertion. An as more than a couple of lines from the proof of its own claim is a bug with a delay on it.
satisfies: check it without widening it
The usual reason people reach for as on an object is that annotating it destroys the detail they wanted:
type Config = Record<string, string | number>;
const config: Config = {
host: 'localhost',
port: 5432,
};
config.port.toFixed(); // Error: 'string | number' has no 'toFixed'The annotation checked the object and then flattened it. as Config avoids the flattening by skipping the check, which is worse. satisfies does both jobs:
const config = {
host: 'localhost',
port: 5432,
} satisfies Config;
config.port.toFixed(); // fine — port is still number
config.hostt; // Error: no such propertyIt verifies the value against the type, and leaves the inferred type alone. That is the operator you want almost every time you were about to write as on a literal.
| You write | Checked? | Type you get |
|---|---|---|
const x: T = { ... } | yes | T — detail lost |
const x = { ... } as T | no | T — detail lost, no check |
const x = { ... } satisfies T | yes | the literal's own type |
The combination worth remembering
satisfies pairs with as const — one keeps the literals narrow, the other proves they are the right ones:
const ROUTES = {
home: '/',
user: '/users/:id',
} as const satisfies Record<string, string>;
type RouteName = keyof typeof ROUTES; // 'home' | 'user'
ROUTES.user; // '/users/:id', not stringAdd a route whose value is a number and the build fails. Read a route and you still get its exact string. Neither annotation nor assertion gives you both.
The rule
Reach for satisfies when you want a value checked, an annotation when you want a value widened to an interface on purpose, and as only with the proof on the previous line. Then make them countable: a codebase where every as is a deliberate, commented exception is one where the type system's claims are still worth something.
Now practice it
2 challenges · 1 quizReading this page is the cheap half. These are the exercises that make you use it.
- Result<T, E> — Discriminated UnionProA discriminated union the caller has to narrow, which is the honest version of asserting what came back.ChallengeSolve · EasyChallengeSolve · Easy
- deepFreeze<T> — Runtime ReadonlyProA signature that has to promise readonly without asserting it — the type and the runtime doing the same job.ChallengeSolve · MediumChallengeSolve · Medium
- Type System · Advanced Type EngineeringAdvanced type questions where the wrong answer is usually the one an assertion would have hidden.QuizTake · HardQuizTake · Hard
Read next
Structural typing: shapes, not names
TypeScript compares shapes, not names. That is why an unrelated object type-checks, why excess properties sometimes error, and how to make a type nothing else can satisfy.
3 min
Literal types, unions and as const
Why a string in a variable stops being a literal, what as const freezes, and why a union of strings beats an enum almost every time.
3 min