14.2 Types you write down
- Approved by
- Panaversity ·
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, 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.
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");$ 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.
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}`);
}$ 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.
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")$ 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 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.
Why strict mode?
It makes sure every value has a type. Both type checkers run in strict mode, a setting from 13.2 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. 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, in your own tool, ten minutes.
Check yourself
Question 1 / 8
0 answered
The type checker passes this agent code, but it prints INV-5317 undefined.type Terms = "Due on receipt" | "Net 15" | "Net 30" | "Net 45" | "Net 60"
const DAYS: Record<Terms, number> = { "Due on receipt": 0, "Net 15": 15, "Net 30": 30, "Net 45": 45, "Net 60": 60 }
const row = { invoice: "INV-5317", terms: "Net 90" }
const terms = row.terms as Terms
console.log(row.invoice, DAYS[terms])What is the bug?
Sources
- Owner
- Panaversity