# Typed locals

> Locals can declare an explicit type instead of var; the declared type pins the binding, and every later assignment to the name is checked against it by the same rule. Literal initializers apply the C# constant conversion; non-literals widen by numeric rank but never across decimal↔double.

<!-- id: function-typed-locals · area: function · stability: stable · html: https://osysharp.com/reference/function/typed-locals/ -->

## Summary        {#summary}
Locals can declare an explicit type instead of `var` — `int x = 5;`, `Order o = …;`,
`List<int> xs = …;`. The declared type **pins** the binding, exactly C#: literal initializers apply the
C# constant conversion, non-literal initializers must be implicitly assignable, and every typed local
must be initialized at its declaration. **The same rule governs every later `x = …`** — the declaration
and the assignment on the next line are checked by one function, so they cannot disagree.

## Signature      {#signature}
```osy syntax
<Type> <name> = <initializer>;
<name> = <value>;                     // …and every later assignment, by the same rule
// Type: a scalar keyword, entity (incl. namespaced), Type?, Type[], Type[][],
//       List<T>/HashSet<T>/Dictionary<K,V>
```

## Description    {#description}
- **A typed local must be initialized at its declaration** — `int x;` is refused (there is no
  definite-assignment analysis; initialize where you declare).
- **Literal initializers use the C# constant conversion**: the literal is re-kinded when widening
  (`int → long/decimal/double`, `decimal → double`) — so `double h = 2.5;` compiles even though a bare
  `2.5` is decimal ([Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/)). A non-representable constant refuses:
  `int x = 2.5m;` → `cannot implicitly convert 'decimal' to 'int'`.
- **Non-literal initializers widen by numeric rank** (`int → long → decimal/double`) but **never across
  decimal↔double** in either direction — C# has no implicit conversion between them
  (`double x = someDecimal;` is a pointed error).
- **`null` needs a nullable declared type** — `int? x = null;`, not `int x = null;`.
- The pinned type drives downstream resolution — an entity-typed local navigates members
  (`Order o = Order.First(); o.Total`).
- **Decompile normalizes to `var`** with the re-kinded literal (`decimal d = 5;` → `var d = 5m;`) —
  semantically identical; the same normalization const-inlining uses.

### Assigning to it afterwards    {#assignment}
**Every assignment is checked against the slot, by the rule above.** `int x = 1; x = "hello";` is refused, and the
message names the variable, what it holds, what you tried to store and the line the type was decided on. This holds
wherever the value lands and however it is spelled:

| The slot | Example |
|---|---|
| a typed local, or one inferred by `var` | `int x = 1; x = "hello";` |
| a parameter | `void F(int p) { p = "hello"; }` |
| a `for` / `foreach` / `catch` variable | `foreach (var i in xs) { i = "hello"; }` |
| a compound assignment | `x += "hello";` |
| a chain | `a = b = "hello";` |
| a component field, with or without `this.` | `counter = "hello";` inside an `action` or a render lambda |
| a dictionary, list or array element — and the key | `d[42] = 1;` on a `Dictionary<string, int>` |
| a class indexer's `set` | `bag["a"] = "hello";` on `int this[string key]` |

The conversions it ACCEPTS are the same ones a declaration, an argument and a `return` accept: the C# constant
conversion for a numeric literal (`decimal d = 0; d = 1;` stores a decimal), numeric widening by rank, and the
implicit upcast to a base type. `decimal ↔ double` is refused in both directions, as in C#.

## Examples       {#examples}
```osy title="typed locals" test app=typed-locals
entity Order { decimal Total; }

decimal Examples() {
  int x = 5;                         // pins int
  long big = 5;                      // constant conversion: the int constant becomes long
  decimal d = 5;                     // → 5m
  double h = 2.5;                    // works — the constant converts (a bare 2.5 is decimal)
  int? maybe = null;                 // nullable declared type accepts null
  Order? o = Order.FirstOrDefault(); // entity-typed local — `?`, because …OrDefault() may answer null
  if (o != null) { return o.Total + d; }
  return d + x + big + maybe ?? 0;
}
```

```osy title="…and the same rule on every assignment after it" test app=typed-locals
decimal Later() {
  int x = 1;
  x = 2;                             // fine — same type
  decimal d = 0;
  d = 1;                             // fine — the int constant becomes a decimal, as at a declaration
  long big = 0;
  big = x;                           // fine — int widens to long
  // x = "hello";                    // refused: cannot assign 'string' to `x`, which holds 'int'
  // d = 1.5;   ⟵ fine too; but `double h = 0; h = d;` is refused — no decimal↔double conversion
  return d + big;
}
```

⚠ **The `?` on `o` is the example, not a typo.** A `…OrDefault()` read answers null when nothing matches, so a
non-nullable `Order o` would be holding a null the moment the table is empty — and every later read of it would be an
unguarded one. The compiler refuses that declaration and names both honest choices: `Order? o` if absent is a case you
handle, or `First()` if it is not, which fails loudly at the read instead of handing you a null that surfaces
somewhere else. This page carried the non-nullable form until 2026-08-27, when the check that catches it landed.

## See also       {#see-also}
- [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) — the literal kinds the constant conversion re-kinds between
- [const](https://osysharp.com/reference/function/const/) — `const Type name = …;` (the compile-time-constant sibling)
