Concept
The beginner framing: this final topic pulls together the domain's recurring threads, inference-first thinking, unknown over any, and knowing when not to add a type, into a set of concrete, checkable practices for writing TypeScript that's both safe and pleasant to work with.
satisfies vs. annotation vs. assertion, precisely
interface Config { mode: string | number; retries: number }
const annotated: Config = { mode: "dark", retries: 3 };
// annotated.mode is `string | number`, WIDENED to Config's full declared type
const asserted = { mode: "dark", retries: 3 } as Config;
// NO validation at all, this compiles even if the object is missing `retries` entirely
const validated = { mode: "dark", retries: 3 } satisfies Config;
// validated.mode is `string`, its OWN specific inferred type, NOT widened to string | numberinterface Config { mode: string | number; retries: number }const config: Config = { mode: "dark", retries: 3 };// : Config, a type ANNOTATION
With a plain `: Config` annotation, `config.mode`'s type becomes exactly what `Config` declares, `string | number`, even though the actual assigned value, `"dark"`, is specifically a string. Using `config.mode` as a plain `string` afterward now requires narrowing first.
Confirmed by compiling all three and checking the resulting types precisely: a plain : Config annotation widens mode to the interface's full declared type (string | number), even though the actual value assigned is specifically a string, using it afterward as a plain string requires narrowing first. as Config performs no structural validation whatsoever; a genuinely mismatched object (missing retries, say) still compiles. satisfies Config gets the real benefit of both: it validates the object actually conforms to Config (a real compile error if it doesn't), while letting the variable keep its own more specific inferred type, here, string, since that's what "dark" actually is, rather than widening to the interface's broader declared type. This is worth stating precisely, since it's commonly overstated: satisfies does not automatically preserve every literal value the way as const does, a plain { mode: "dark" } satisfies still infers , not the literal , unless combined with . What actually guarantees is that the result keeps whatever type inference would have produced anyway, including correctly distinguishing which member of a union (like 's ) a given property actually is, without an annotation forcing it back to the wider declared type.
unknown at trust boundaries
async function fetchUser(id: number): Promise<unknown> {
const res = await fetch(`/api/users/${id}`);
return res.json(); // the ACTUAL shape is unverified, unknown is honest about that
}
const data = await fetchUser(1);
if (typeof data === "object" && data !== null && "name" in data) {
console.log(data.name); // only accessible after a real narrowing check
}Data crossing a genuine trust boundary, an API response, JSON.parse's result, user input, has a shape TypeScript cannot actually verify at compile time, no matter what type is written down. Typing it unknown (rather than a hopeful, unverified interface, or worse, any) is the honest choice: it forces an explicit runtime check before the data is used, which is the only thing that can actually verify the shape matches reality.
Inference-first, revisited: when NOT to type something
// ❌ redundant, inference already gets this exactly right
const count: number = 0;
const items: string[] = ["a", "b"];
// ✅ necessary, parameters have nothing to infer from
function process(value: number) { return value * 2; }
// ✅ valuable, documents a public API's contract explicitly
export function calculateTotal(items: CartItem[]): number { /* ... */ }This closes the loop back to TypeScript Basics's opening framing: the goal was never "type everything as explicitly as possible", it's using the type system precisely where it adds real safety or real documentation value, and trusting inference everywhere else.
API design for types: think about what callers actually need
// ❌ forces every caller to provide values for options they don't care about
function createUser(name: string, email: string, role: string, isActive: boolean, sendWelcomeEmail: boolean) { }