# 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, and where text from outside becomes one of those types."
status: stable
order: 314.02
ksor:
  owner: team:panaversity
  audience: [ public ]
  approval:
    by: process:panaversity
    at: 2026-11-10T15:33:15Z
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-10T15:33:15Z
  by: process:panaversity
trust_tier: unverified
build_id: sha256:46fd53c5d7ab1c14f6347908d45c9fdaecda4d914439923ee878bdcecd57803e
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 checkboxes for delivery: ground, two-day, overnight. Paper cannot stop anyone writing "whenever", so a clerk checks each answer once, when the form arrives.

**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), a type label said what kind of value a variable held. Here you decide the shape of your own data, and the agent writes it down. A record's **fields** are the pieces of data it holds. 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"` |

A literal type is the form's row of checkboxes: only these answers exist. Here it is for the terms, with two calls. Predict which call the checker refuses.

```ts tab="TypeScript" tab-group="language"
// the checkboxes: Terms is one of these five values, and | means "or"
export type Terms = "Due on receipt" | "Net 15" | "Net 30" | "Net 45" | "Net 60";

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

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

```python tab="Python" tab-group="language"
from typing import Literal

# the checkboxes: Terms is one of these five values
Terms = Literal["Due on receipt", "Net 15", "Net 30", "Net 45", "Net 60"]

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

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

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

```text tab="Python" tab-group="language"
$ 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"
```

The checker names the file, then the line and column of `"Net 10"`. An agent calling `dueDate` cannot pass `"net 30"` in lowercase either, 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 will not accept it, because it might be `"Net 10"`. The type checker reads your code before the program runs, and never sees what arrives in a file.

So the program needs a **type guard**, such as Brightline's `isTerms`: code that runs with the program and reads the text that came in. It is the form's clerk: it answers true or false, once, where the text enters. The checker believes a true answer, and treats the text as `Terms`.

A **cast**, such as `as Terms`, skips the clerk: it tells the checker to believe the text without looking.

Here is the clerk. Predict both answers.

```ts tab="TypeScript" tab-group="language"
// the clerk: takes any text, and answers whether it is one of the Terms.
// "text is Terms" tells the checker to believe a true answer
function isTerms(text: string): text is Terms {
  // is the text on the list?
  return ["Due on receipt", "Net 15", "Net 30", "Net 45", "Net 60"].includes(text);
}

console.log(isTerms("Net 30"), isTerms("Net 10"));
```

```python tab="Python" tab-group="language"
from typing import TypeGuard

# the clerk: takes any text, and answers whether it is one of the Terms.
# TypeGuard[Terms] tells the checker to believe a true answer
def is_terms(text: str) -> TypeGuard[Terms]:
    # is the text on the list?
    return text in ["Due on receipt", "Net 15", "Net 30", "Net 45", "Net 60"]

print(is_terms("Net 30"), is_terms("Net 10"))
```

```text tab="TypeScript" tab-group="language"
true false
```

```text tab="Python" tab-group="language"
True False
```

**At work.** Ana has the agent change one line: the `terms` parameter of `dueDate` goes from `string` to `Terms`. The type checker reports six errors. Four pass raw text from a file straight into the calculation. One is the test that checks unknown terms on purpose. One is a call an agent wrote with `"Net 10"`, terms Brightline has never used. Ana decides two things, and the agent makes both changes: check the text once, where it is read, and have that one test expect the error.

### 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. Libraries such as Pydantic use them to check data at run time, as [14.4](data-models-and-schemas.md) shows. So an ordinary type protects you only if the type checker runs, which is why it runs on every change.

### Try it

The file in `14-2` has a small `Terms` type and two calls. Run it and investigate, as in [13.1](../programming-fundamentals/you-read-the-agent-writes.md#try-it).

**Run the checker.** Send: "Please run the file, then check it with the type checker. Show me what each prints." The file runs, and the checker refuses one line.

**Modify.** Send: "Please write the second call with a cast, so the checker believes it: `as Terms` in TypeScript, or `cast(Terms, ...)` in Python. Don't run anything yet." Will the checker accept it now? What will the file print? Then ask the agent to run the checker and the file.

**Make.** Send: "Please add a function that takes text, answers whether it is one of the allowed terms, and reports it if not. Use it for "Net 10" instead of the cast."

**What you should see.** The checker accepts the cast, and the run is still wrong: `undefined` in TypeScript, a `KeyError` in Python.

**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.

![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. `parseTerms` is the same check, but it throws an error where `isTerms` answers false.*

**Next.** A type describes one value. [14.3](classes-and-objects.md) holds several values together, with the functions that work on them.
