TypeScript checks your code, then the types disappear. What runs is plain JavaScript. Most of the time that's invisible. The bugs live where it isn't.
The fastest way to build intuition is to run typed code and look at the values, not the types.
as is a promise, not a check
type User = { name: string; age: number }; const raw: unknown = JSON.parse('{"name":"Ada","age":"36"}'); const user = raw as User; user.age + 1; The editor is happy: user.age is a number. The value is "361". The JSON had "36" as a string, as User didn't check anything, and + concatenated.
as tells the compiler "trust me". Nothing at runtime holds you to it.
Type guards: a check the compiler believes
A type guard is a function that checks at runtime and tells the compiler what it proved:
function isUser(value: unknown): value is User { return ( typeof value === "object" && value !== null && typeof (value as User).name === "string" && typeof (value as User).age === "number" ); } isUser(raw); // false isUser({ name: "Ada", age: 36 }); // true Inside if (isUser(raw)) { … }, raw is a User, and this time it's true.
Assertion functions: fail loudly
When bad data should stop the program, an assertion function narrows the type for everything after it:
function assertUser(value: unknown): asserts value is User { if (!isUser(value)) { throw new TypeError(`Not a User: ${JSON.stringify(value)}`); } } assertUser(raw); // TypeError: Not a User: {"name":"Ada","age":"36"} One gotcha: assertion functions need an explicit type at the call site, so declare them with function (or annotate the const), or TypeScript refuses to narrow.
satisfies: check without widening
satisfies checks a value against a type but keeps the precise type you wrote:
const themes = { graphite: { dark: true }, "graphite-light": { dark: false }, } satisfies Record<string, { dark: boolean }>; themes.graphite.dark; // fine // themes.nord // error: no such key Annotate it as Record<string, …> instead and themes.nord compiles, then is undefined at runtime.
Watching it happen
I write these in JSLab, a JavaScript & TypeScript scratchpad for macOS. Two things make it good for this:
- Types are checked in the editor and stripped when the code runs, so a type error is an underline, never a failed run. You can write
const n: number = "5";, see the underline and see that the value is"5". - Auto Log shows the value of each top-level expression beside its line. Every
user.age + 1;above prints its real answer without aconsole.log.
Here's Auto Log in a real JSLab TypeScript tab: every expression on its own line shows its value beside it, xs.sort() on line 3, xs.length on line 4, through xs.at(-1) on line 11, with no console.log anywhere.
Thrown errors land on their line too, so assertUser(raw) shows up as an error row tagged with the line that called it.
The short version
-
asand!are claims. Use them when you know, not when you hope. - At the edges (JSON,
fetch,localStorage), check with a guard or an assertion. - Prefer
satisfiesover an annotation when you want both the check and the precise type.
JSLab launches Nov 15. TypeScript in the editor: https://js-lab.app/docs/editor


