# The Type System, Deep - Structural Typing & How Inference Works

You've spent eight phases *using* TypeScript's types. This phase covers how the checker actually *thinks* - the rules it follows when deciding whether one type fits another, and where the types you never wrote come from. It's the advanced half of the guide, and it pays off everywhere: once you understand these mechanics, the checker's most baffling errors (and its most surprising silences) become predictable.

Here's the mental model to carry through the whole phase: **TypeScript judges types by their *shape*, not their name, and it does an enormous amount of guessing on your behalf.** Most of the time you never write a type annotation - TypeScript infers one. Knowing *which* type it infers, and *why*, is the difference between fighting the checker and steering it.

## Structural typing - "duck typing for types"

The first thing to internalize is how TypeScript decides whether two types are the same, or compatible. Coming from Java or C#, you'd expect this to hinge on names - a value is a `Point` only if declared as one. TypeScript does the opposite.

📝 **Structural typing** - types are compared by their *structure* (the members they have), not their declared name. If a value has at least all the members a target type requires, it's compatible with that target, even if the two were never related on paper. The contrast is **nominal typing** (Java, C#), where compatibility depends on the explicit name in the declaration.

You may know this idea from runtime languages as "duck typing": if it walks like a duck and quacks like a duck, treat it as a duck. TypeScript applies the same principle to *static* types.

```typescript
interface Point {
  x: number;
  y: number;
}

function printPoint(p: Point): void {
  console.log(`(${p.x}, ${p.y})`);
}

// Never declared as a Point - just an object with x and y.
const location = { x: 10, y: 20 };
printPoint(location); // ✅ accepted

// A class that has no idea Point exists.
class Vector {
  constructor(public x: number, public y: number) {}
}
printPoint(new Vector(3, 4)); // ✅ also accepted
```

*What just happened:* Neither `location` nor `Vector` mentions `Point` anywhere. In Java this would be a compile error - they're not declared to implement the interface. TypeScript only checks the *shape*: does this value have an `x: number` and a `y: number`? Both do, so both are valid `Point`s. The name is just a label for humans; the checker reasons entirely about members.

💡 **Why this design.** JavaScript objects are bags of properties created on the fly - from JSON, literals, libraries that never heard of your types. Nominal typing would reject all of them. Structural typing lets TypeScript describe code that already exists without retrofitting `implements` clauses onto every object - the rule that makes it practical to bolt onto real JavaScript.

## Assignability & the excess-property surprise

Structural typing is governed by one core question: **is type X assignable to type Y?** Get this phrasing right and most type errors decode themselves.

📝 **Assignability** - X is assignable to Y if X has *at least everything Y requires*. Y is the contract ("I need an `x` and a `y`"); X satisfies it as long as it provides those, regardless of extra members. More is fine; missing a required one is not.

That "more is fine" direction surprises people, so look at it directly:

```typescript
interface Named {
  name: string;
}

const employee = { name: "Ada", salary: 90000 };

const n: Named = employee; // ✅ employee has name (plus extra) - assignable
```

`Named` requires only a `name: string`. `employee` has that, plus a `salary` field, which doesn't disqualify it - `employee` provides everything `Named` asks for, so it's assignable. Through the variable `n`, TypeScript only lets you see `.name`, but the value underneath still carries `salary` at runtime.

Now the gotcha that bites everyone:

⚠️ **Excess-property checks fire on object *literals* only.** When you assign a fresh object literal directly to a typed target, TypeScript runs an *extra* check that rejects properties the target doesn't declare - contradicting the "more is fine" rule above, on purpose, to catch typos. Route the same object through an intermediate variable and the check vanishes.

```typescript
interface Options {
  width: number;
  height: number;
}

// Direct literal - excess-property check fires.
const a: Options = { width: 100, height: 50, depth: 10 };
```
```console
Object literal may only specify known properties, and 'depth' does not
exist in type 'Options'.
```

The fix - and the reason the rule exists - is assigning through a variable, which downgrades the check to ordinary assignability:

```typescript
interface Options {
  width: number;
  height: number;
}

const raw = { width: 100, height: 50, depth: 10 };
const b: Options = raw; // ✅ no excess-property check - plain assignability
```

The first version hands a brand-new literal straight to an `Options` variable. TypeScript assumes a literal written *right there* should match the target exactly, so the stray `depth` is almost certainly a typo (maybe you meant `width`?) and it errors. The second version assigns `raw` first; by the time it reaches `b`, it's a *value*, not a literal, so the normal rule applies - `raw` has everything `Options` needs, extra `depth` and all, so it's assignable. Same object, different rule: one is a literal at the point of assignment, the other isn't.

💡 The excess-property check is a deliberate, narrow exception to structural typing - aimed at the single most common mistake, a misspelled property name in a literal. When you genuinely want the extra property, the intermediate-variable form tells the checker "I meant to do this."

## Type widening - why `let` and `const` infer differently

When you don't annotate, TypeScript infers a type. But it doesn't always infer the *narrowest* possible one - it sometimes **widens** a literal to its general type, depending on whether the binding can change.

📝 **Type widening** - when TypeScript infers a type from a literal value, it broadens (widens) the literal to its general type for mutable bindings. `let x = "hi"` infers `string`, not the literal type `"hi"`, since you might reassign `x` later. A `const` can never be reassigned, so `const x = "hi"` keeps the exact literal type `"hi"`.

```typescript
let mutable = "hi";       // inferred type: string
const immutable = "hi";   // inferred type: "hi"  (a literal type)

mutable = "bye";          // ✅ fine - string accepts any string
// immutable = "bye";     // would error - "hi" accepts only "hi"

let count = 42;           // inferred: number
const max = 42;           // inferred: 42
```

`mutable` is a `let`, so it could be reassigned to any other string - inferring the locked-down literal `"hi"` would make `mutable = "bye"` an error, which would be absurd. TypeScript widens it to `string`. `immutable` is a `const` that physically cannot change, so TypeScript keeps the most precise type it knows, the literal `"hi"`. Same split for `count` (`number`) versus `max` (`42`).

💡 **Why this matters.** Literal types power discriminated unions, exhaustive `switch` checks, and precise function arguments. When you *want* that precision, `const` preserves it; when you want flexibility, `let` gives you the general type. The kind of binding you choose silently shapes the type you get.

## `as const` - freezing a value to its narrowest types

`const` only stops *reassignment of the variable*. It does nothing for the *contents* of an object or array - those still get widened, member by member. To freeze everything inside to its exact literal types, reach for `as const`.

📝 **`as const`** - a *const assertion* applied to a value. It tells TypeScript to infer the narrowest possible type: every member becomes its exact literal type, and the whole structure becomes deeply `readonly`. It's the tool for fixed configuration objects and discriminated-union tags.

Watch the difference:

```typescript
// Without as const - members are widened.
const config1 = { mode: "dark", retries: 3 };
// inferred: { mode: string; retries: number }
//   config1.mode is just string - "light", "anything" would type-check

// With as const - members are pinned and readonly.
const config2 = { mode: "dark", retries: 3 } as const;
// inferred: { readonly mode: "dark"; readonly retries: 3 }
//   config2.mode is exactly "dark", and you can't reassign it
```

In `config1`, the outer `const` stops you from reassigning the whole variable, but each property is inferred with widening - `mode` becomes `string`, `retries` becomes `number`. In `config2`, `as const` flips every member to its literal type (`"dark"`, `3`) and marks them all `readonly`: a precise, immutable description of itself.

The canonical fix for losing literal types where you need them - most often a union tag:

```typescript
type Action =
  | { type: "increment"; by: number }
  | { type: "reset" };

// Without as const, `type` widens to string and won't match the union.
const bad = { type: "increment", by: 1 };
// const result1: Action = bad; // ❌ string not assignable to "increment"

const good = { type: "increment", by: 1 } as const;
const result2: Action = good; // ✅ type is exactly "increment"
```

`Action` is a discriminated union keyed on the literal `type` field. Plain inference widens `bad.type` to `string`, which fits neither branch, so the assignment fails. `as const` keeps `good.type` as the literal `"increment"`, matching the first branch, and the assignment succeeds. Any time an object needs to *be* a specific union member, `as const` is how you keep its tag literal.

## How inference flows - let TypeScript do the work

You've seen TypeScript infer from values. It also infers from *context* - surrounding code tells it what a type should be, so you don't have to annotate.

📝 **Contextual typing** - TypeScript infers a value's type from the position it appears in. The classic case: a callback's parameter types are inferred from the function that receives it.

```typescript
const nums = [1, 2, 3];

// No annotation on n - TypeScript knows nums is number[],
// so .map's callback parameter must be a number.
const doubled = nums.map((n) => n * 2); // n: number, doubled: number[]

const words = ["a", "bb", "ccc"];
const lengths = words.map((w) => w.length); // w: string, lengths: number[]
```

You never wrote a type for `n` or `w`. Because `nums` is `number[]`, `.map`'s callback parameter is fixed to `number` by context, so `n` is a `number`; `words` is `string[]`, so `w` is a `string`. The *return* type is inferred too - `n * 2` is a `number`, so `doubled` is `number[]`. Annotating any of these would be noise; context already pins them down.

Return-type inference works the same way for your own functions - TypeScript reads the `return` statement:

```typescript
function makeUser(name: string, age: number) {
  return { name, age, active: true };
  // inferred return type: { name: string; age: number; active: boolean }
}
```

You annotated the *parameters* (the boundary, where data enters) but not the return - TypeScript computed that from the object you return. Adding `: { name: string; age: number; active: boolean }` would just restate what it already knows, and risk drifting out of sync if you later add a field.

💡 **The practical rule.** Let inference do the work *inside* your code; annotate at the *boundaries* and when you want to *pin* a type. Annotate function parameters and exported/public signatures so callers get a stable contract and errors point at the right place. Skip annotations on local variables, callback parameters, and clearly-correct return types - over-annotating is a common beginner habit that adds noise and lets types drift from reality.

One modern tool deserves a mention here: sometimes you want to *check* a value against a type without *widening it to that type* - keeping the precise inferred type for later use. That's `satisfies`:

```typescript
type Theme = Record<string, string>;

// `: Theme` would widen palette to Record<string, string>,
// losing the specific keys. `satisfies` checks AND keeps them.
const palette = {
  primary: "#2563eb",
  danger: "#dc2626",
} satisfies Theme;

palette.primary; // ✅ still known to exist
// palette.missing; // ❌ caught - not a key of palette
```

Annotating `const palette: Theme` would verify the shape but then treat `palette` as a plain `Record<string, string>`, forgetting the specific keys (`primary`, `danger`). `satisfies Theme` runs the same compatibility check - every value must be a `string` - but leaves `palette`'s narrow inferred type intact, so you keep autocomplete and key-existence checks: "validate against a type, but don't widen to it."

## Recap

1. **Structural typing** - TypeScript compares types by their *shape* (members), not declared name. An unrelated object or class is accepted anywhere its members satisfy the target - "duck typing" for static types, and what makes TS fit real JavaScript.
2. **Assignability** means X provides *at least everything Y requires* - extra properties are fine. The exception is the **excess-property check**, firing only on object *literals* assigned directly to a typed target (to catch typos); an intermediate variable downgrades it to plain assignability.
3. **Widening**: inference from a literal broadens to the general type for mutable bindings (`let x = "hi"` → `string`) but keeps the exact literal for `const` (`const x = "hi"` → `"hi"`), since a `const` can never change.
4. **`as const`** freezes a value to its narrowest literal types and makes it deeply `readonly` - essential for fixed config and for keeping discriminated-union tags literal instead of widened to `string`.
5. **Inference flows from context**: callback parameters are typed by where they're used (`arr.map(x => ...)` knows `x`), and return types come from `return` statements. Annotate boundaries, let inference handle the inside; `satisfies` checks a value against a type without widening it.

With the checker's reasoning demystified, you're ready to *transform* types programmatically - utility and mapped types build new types out of existing ones, leaning directly on the assignability and inference rules you just learned.

## Quick check

Lock in the ideas most likely to trip you up - shape-based compatibility, the excess-property exception, and what `as const` preserves:

```quiz
[
  {
    "q": "A function expects a `Point` (interface with `x: number; y: number`). You pass `new Vector(3, 4)`, a class that never mentions `Point`. Why does TypeScript accept it?",
    "choices": [
      "TypeScript uses structural typing - Vector has the required `x` and `y` members, so its shape matches `Point` regardless of its name",
      "TypeScript silently converts the Vector into a Point at runtime",
      "Classes are exempt from type checking when passed to functions",
      "It only works because Vector and Point happen to start with similar letters"
    ],
    "answer": 0,
    "explain": "TypeScript compares by shape, not name. `Vector` has `x: number` and `y: number`, which is everything `Point` requires, so it's assignable. The declared name is irrelevant - that's structural (duck) typing."
  },
  {
    "q": "`const a: Options = { width: 100, height: 50, depth: 10 }` errors on `depth`, but assigning the same object through a variable first does not. Why?",
    "choices": [
      "Excess-property checks fire only on object literals assigned directly to a typed target; an intermediate variable falls back to ordinary assignability, where extra properties are allowed",
      "The variable version deletes the `depth` property automatically",
      "Object literals are immutable and variables are not, so the rules differ",
      "It's a compiler bug - both forms should error"
    ],
    "answer": 0,
    "explain": "The excess-property check is a special case aimed at catching typos in literals. It applies only to a literal assigned straight to a typed slot. Through a variable, the normal 'at least everything required' rule applies, and extra properties are fine."
  },
  {
    "q": "You write `const action = { type: \"increment\", by: 1 }` and try to assign it to a discriminated union keyed on `type`. It fails. What fixes it?",
    "choices": [
      "Add `as const` so `type` keeps its literal type \"increment\" instead of being widened to `string`",
      "Change `const` to `let` so the type becomes mutable",
      "Remove the `by` field so the object is smaller",
      "Nothing - discriminated unions can't be built from object literals"
    ],
    "answer": 0,
    "explain": "Plain inference widens `type` to `string`, which matches no branch of the union. `as const` pins every member to its exact literal type, so `type` stays \"increment\" and the object matches that branch of the union."
  }
]
```
