# 14.2 Types you write down (/building-ai-workers/structuring-programs/types-you-write-down)

---
type: Document
title: "14.2 Types you write down"
description: "How to write down which values your data may hold, where text from outside becomes one of those types, and why strict mode matters."
status: stable
order: 314.02
ksor:
  owner: team:panaversity
  audience: [ public ]
  approval:
    by: process:panaversity
    at: 2026-11-02T08:13:58Z
chapter: "14"
part: III
expert_status: builder-only
concepts: [ "14.2" ]
last_verified: 2026-10-30
sources:
  - id: tsconfig-reference
    title: "TSConfig reference (TypeScript, accessed 2026-10-30)"
    resource: https://www.typescriptlang.org/tsconfig/
generated:
  at: 2026-11-02T08:13:58Z
  by: process:panaversity
trust_tier: unverified
build_id: sha256:e1fbcb7d9a8f8b9b7cb96875022325c02c9ba4be94cef13b301cbd39a9edad85
dirty: true
ksor_version: 0.0.60
---

**In everyday life.** A paper form has boxes for the date, with "MM/DD/YYYY" printed under them, and a row of checkboxes for the delivery method: ground, two-day, overnight. Paper cannot stop anyone writing "whenever", so the clerk who enters the form checks each answer once, when it arrives. Its shape says what answers are allowed before anyone fills it in.

**What you will learn.** How to write down which values your data may hold, and where text from outside becomes one of them.

**Why it matters.** Once those values are written down, the type checker checks every later change against them, the agent's included.

### What can a type say that a name cannot?

Exactly which values are allowed. In [Chapter 13](../programming-fundamentals/values-types-variables.md), type labels said what kind of value each variable held. Here you decide the shape of your own data, and the agent writes it down, so the type checker can check every line of code against it. A record's **fields** are the pieces of data it holds, such as an invoice's number and amount. Four kinds of type do most of the work.

| Kind | Reads as | Brightline example |
| --- | --- | --- |
| A record type | "An invoice has these fields, of these types" | `Invoice`, with `number`, `vendorId`, `amountCents` and four more |
| A **union** | "This, or that" | `string \| null`: a due date, or nothing |
| An optional field | "This field may be missing" | A vendor's remit email, which may be missing |
| A **literal type** | "Only these exact values" | Terms: `"Net 15"`, `"Net 30"`, `"Net 45"`, `"Net 60"` or `"Due on receipt"` |

**Types tell the agent too.** A literal type in a signature is a stronger instruction than a sentence in a prompt. An agent asked to call `dueDate` with terms of type `Terms` cannot pass `"net 30"` in lowercase, because the type checker refuses it.

### Where does text from a file become a type?

Where it enters the program. Text read from a file is only a string. A literal type for terms will not accept a string, because the string might be `"Net 10"`. So two checks work together, at two different times. The type checker reads your code before the program runs. It sees what is typed in the code, and never what arrives in a file. A **type guard** is code that runs while the program runs, and it reads the text that came in. It is the form's clerk: it answers true or false, once, where the text enters. A **cast**, such as `as Terms`, skips the clerk: it tells the checker to believe the text without looking.

**At work.** Ana asks the agent to change one line: the `terms` parameter of `dueDate` goes from `string` to `Terms`. The type checker then reports six errors. Four are places where raw text from a file was passed straight into the calculation. One is the test that checks unknown terms on purpose. And one is a call an agent wrote with `"Net 10"`, terms Brightline has never used. The agent fixes each place as she decides, by checking the text once, where it is read, and marks that test as expecting the error.

**In TypeScript.** An excerpt from `src/due-date.ts`, then two calls. `type` names a type, and `|` means "or". `TERMS_DAYS` is the table of allowed terms and their days, and `Object.hasOwn` asks whether the table has that name. Predict which call the checker refuses.

```ts
export type Terms = "Due on receipt" | "Net 15" | "Net 30" | "Net 45" | "Net 60";

export function isTerms(text: string): text is Terms {
  return Object.hasOwn(TERMS_DAYS, text);
}

export function dueDate(invoiceDate: string, terms: Terms): string { /* ... */ }

dueDate("2026-10-01", "Net 30");
dueDate("2026-10-01", "Net 10");
```

```text
$ npm run typecheck
exercises/wrong-call.ts(4,35): error TS2345: Argument of type '"Net 10"' is not assignable to parameter of type 'Terms'.
```

The checker names the file with the wrong call, `exercises/wrong-call.ts`, then the line and column of `"Net 10"` in it. `text is Terms` makes `isTerms` a type guard. Here it is at work on text from a file. Predict which call the checker refuses.

```ts
import { dueDate, isTerms } from "../src/due-date.ts";

const text: string = "Net 30"; // as if read from a file
dueDate("2026-10-01", text);
if (isTerms(text)) {
  dueDate("2026-10-01", text);
} else {
  console.log(`Unknown terms: ${text}`);
}
```

```text
$ npm run typecheck
exercises/from-file.ts(4,23): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Terms'.
```

The checker refuses the first call, because `text` is only a string there. It accepts the second, because it can see the `if` in the code: that line runs only when `isTerms` answers true. So inside the `if`, it treats `text` as the narrower type, `Terms`, one that allows fewer values, and every function called there can trust it. If the file held `"Net 10"`, `isTerms` would answer false while the program runs, and the `else` would report it.

**In Python.** An excerpt from `due_date.py`, then one call. `Literal` lists the allowed values, and `TypeGuard` does the job of `text is Terms`. `get_args(Terms)` gives the five allowed values.

```python
from typing import Literal, TypeGuard, get_args

Terms = Literal["Due on receipt", "Net 15", "Net 30", "Net 45", "Net 60"]

def is_terms(text: str) -> TypeGuard[Terms]:
    return text in get_args(Terms)

def due_date(invoice_date: str, terms: Terms) -> str: ...

due_date("2026-10-01", "Net 10")
```

```text
$ uv run pyright
wrong_call.py:5:30 - error: Argument of type "Literal['Net 10']" cannot be assigned to parameter "terms" of type "Terms" in function "due_date"
```

### What does a type do while the program runs?

Nothing, by itself. Node removes TypeScript's type labels before the program runs. Python keeps its type hints, the labels of 13.2, but does not enforce them by itself. Libraries such as Pydantic use them to check data at run time, as [Concept 14.4](data-models-and-schemas.md) shows. Either way, an ordinary type protects you only if the type checker runs, which is why it runs on every change.

**Where the languages differ.** In TypeScript, a record type can be written with `type` or with `interface`. The two say almost the same thing, and you will see both in agent code. Python writes a record type as a class, which is [Concept 14.3](classes-and-objects.md).

### Why strict mode?

It makes sure every value has a type. Both type checkers run in **strict mode**, a setting from [13.2](../programming-fundamentals/the-workbench.md#why-strict-mode) that makes them check more. So an agent cannot leave a function or an empty list without a type. For example, `vendors = []` fails in Python until it says what the list holds, `vendors: list[Vendor] = []`. That matters most here, because a value with no type is accepted anywhere, even where only `Terms` may go, so the type checker lets it through without a check.

**The operating rule:** write the allowed values into the types, check raw text once where it enters the program, and let the type checker check every line after that against the types.

![Figure 14.2. Text going through a check during the run before it becomes Terms. "Net 30" matches exactly and gets in. "Net 10" is not an allowed value and "net 30" has the wrong case, so both are rejected and reported. isTerms answers true or false, and parseTerms returns Terms or throws an error. The five allowed terms are Due on receipt, Net 15, Net 30, Net 45 and Net 60.](img/type-fence.png)

*Figure 14.2. The clerk's check, in code. Text from outside is checked once as it comes in, and the code inside can rely on it. That is true as long as the check is right and no code inside skips it with a cast, such as `as Terms`. `parseTerms` is the same check, but it throws an error where `isTerms` answers false.*

**Practice it.** [Practice: types you write down](both-ai-vendors.md#practice-types-you-write-down), in your own tool, ten minutes.
