The error you already know

You type every line the way you learned to in Python, where a bare name tells the next reader nothing and spelling out intent is just being kind. So you write const model: string and feel safer for it. The compiler had already read the value on the right and knew more than the annotation says.

build-request.ts
// You type every line, so nothing is left
// for the next reader to guess.
const model: string = "gpt-4o";
const temperature: number = 0.7;
function callModel(opts: {
model: "gpt-4o" | "gpt-4o-mini";
temperature: number;
}) {
return opts;
}
callModel({ modelType 'string' is not assignable to type '"gpt-4o" | "gpt-4o-mini"'., temperature });

Written bare, const model = "gpt-4o" infers the literal type "gpt-4o", which is exactly what callModel asks for. The : string you added to be careful is what widened it back to any string and broke the call. The annotation threw away what the compiler already knew.

build-request.ts(9,13) Type 'string' is not assignable to type '"gpt-4o" | "gpt-4o-mini"'.

Watch the habit become the idiom

The reflex says more annotations mean more safety. Inside a function, where the value sits right next to the name, the opposite is true. The compiler reads the literal, the number, the array, and lands on a type at least as narrow as anything you would have written by hand.

Step through it at your own pace. Each click is one move.

Step 1 of 4

The habit, every local annotated even when the value says it all.

build_request.pyPython
def build_request(prompt):
    model: str = "gpt-4o"
    temperature: float = 0.7
    tools: list = ["search", "calculator"]
    return {
        "model": model,
        "temperature": temperature,
        "tools": tools,
    }

Here's what each move does.

  • Start with the habit, every local spelled out the way a bare Python name forces you to.
  • Port it across and the annotations come along, each one naming a type the value had already fixed.
  • Delete them. Inference reads the literal and the array and lands on types just as narrow, sometimes narrower than what you typed.
  • Keep the one annotation that earns its place, the signature other code reads.

That last move is the whole point. Inference covers the inside of a function, where you and the compiler can both see the value. Annotations earn their keep at the edges, a function's parameters and return, an exported type, the public surface another file imports against. Those are the spots where the reader cannot see the value and the compiler should not have to guess your intent.

Now make the compiler agree

Three constants below are annotated with types their values already imply, and buildRequest reads all three. Delete the redundant annotations and let inference do the reading. Leave the parameter type in place, because that is the boundary a caller depends on. The hidden checks confirm every type the annotations promised still holds once they are gone, so a real change shows up rather than a pass.

Loading...
type-checking…