Structures and types: the shape of subject data
This chapter is about what a function’s input is made of: built-in types, optional fields, states and nested records. And about an honest boundary: what the compiler checks by itself and what is left to the application.
Minimal example
модуль «Счета»
тип «Валюта»
вариант «Рубль»
вариант «Доллар»
вариант «Евро»
объект «Строка счёта»
«номер»: строка
«сумма»: неотрицательное
«валюта»: «Валюта»
«срок оплаты»: строка
«просрочен»: признак
комментарий иногда является строкой
объект is a record: a set of fields under one name. It has no behaviour — no
methods, no constructor, no default values. A field is written as
«имя»: тип or in the form имя является типом; иногда является gives a
field the right to be absent.
A тип with variants is a closed list of values. The currency here is not the
string "RUB" but one of three variants, and a typo in a variant name will not
pass the check.
What the compiler does
flang check parses the declarations and checks the types. A field’s type
must be known: built in, declared in the module, or brought in with использует.
If the type is declared nowhere, the file does not pass:
модуль «Счета»
объект «Строка счёта»
«номер»: строка
«валюта»: «Валюта»
FLANG_UNKNOWN_NAME … неизвестный тип «Валюта»
flang ast prints the parsed model — JSON with the types and functions. Form,
table and service contract adapters read it. For the minimal example (the
span and fts fields are left out):
[
{ "kind": "sum", "name": "Валюта",
"variants": [ { "name": "Рубль", "fields": [] }, { "name": "Доллар", "fields": [] }, { "name": "Евро", "fields": [] } ] },
{ "kind": "record", "name": "Строка счёта",
"fields": [
{ "name": "номер", "type": { "kind": "string" } },
{ "name": "сумма", "type": { "kind": "named", "name": "неотрицательное" } },
{ "name": "валюта", "type": { "kind": "named", "name": "Валюта" } },
{ "name": "срок оплаты", "type": { "kind": "string" } },
{ "name": "просрочен", "type": { "kind": "flag" } },
{ "name": "комментарий", "type": { "kind": "string" }, "optional": true }
] }
]
Optionality is a separate flag, "optional": true, not a part of the type
string.
Built-in types
| flang | What it is | In flang ast |
In TypeScript (flang emit --target ts) |
|---|---|---|---|
строка |
text | string |
string |
число |
an IEEE 754 number, NaN included |
number |
number |
неотрицательное |
an integer from 0 to 2⁵³−1 | named |
number |
целое |
an integer with no lower bound | named |
number |
сотых |
money as a whole number of cents | named |
any |
признак |
да or нет |
flag |
any |
список строки |
a list | list |
Array<string> |
иногда является … |
the field may be absent | "optional": true |
the field’s type, with no null in it |
A type is not decoration. It decides what the compiler can prove. A
неотрицательное has two bounds, and a promise like “the total is not less
than a summand” follows for the kernel from the argument types alone. A
число has no bounds, and the same promise stays unproved.
The compiler also accepts the words дата and деньги, but they are not
separate types: дата is the same string ("kind": "string") and деньги
the same number ("kind": "number"). That is why "вчера" passes as a date
just as easily as "2026-07-15":
модуль «Сроки»
объект «Счёт»
«срок»: дата
тотальная функция «Срок оплаты»
принимает счёт: «Счёт»
возвращает строка
пример «любая строка проходит как дата»
дано счёт равно запись «Счёт» с «срок» равным "вчера"
ожидается "вчера"
счёт.«срок»
flang 0.7.29 has no calendar type. The application parses and checks the date
at the boundary, and the model receives an already checked string of the form
ГГГГ-ММ-ДД or a number of days. There is no built-in currency type either —
a currency is declared as its own type with variants, as in the minimal
example.
An optional field
иногда является lets you leave a field out when building a record. The other
side: in 0.7.29 such a field is of little use inside functions. The value read
from it has the type “string or nothing”, and it cannot be compared with
ничто:
FLANG_TYPE … сравнение «eq» между строка или ничто и ничто
If a rule must behave differently depending on whether a value is present, declare a type with variants:
модуль «Заявки»
тип «Комментарий»
вариант «Есть» содержит текст: строка
вариант «Нет»
объект «Заявка»
«имя»: строка
«комментарий»: «Комментарий»
тотальная функция «Нужен ответ менеджера»
принимает заявка: «Заявка»
возвращает признак
пример «без комментария ответ не нужен»
дано заявка равно запись «Заявка» с «имя» равным "Анна" и «комментарий» равным (вариант «Нет»)
ожидается нет
разбор заявка.«комментарий»
случай вариант «Есть» с текст как текст
то да
случай «Нет»
то нет
иногда является stays useful where the field only travels further: into a
form, into a table, into the printed code.
A state is a type with variants
An order’s state is not a string and not a set of flags but a type with variants. A variant may carry fields of its own: a shipped order has a waybill, a created one does not.
модуль «Заказы»
тип «Состояние заказа»
вариант «Создан»
вариант «Оплачен»
вариант «Отгружен» содержит «накладная»: строка
тотальная функция «Можно отгрузить»
принимает заказ: «Состояние заказа»
возвращает признак
пример «оплаченный заказ можно отгрузить»
дано заказ равно вариант «Оплачен»
ожидается да
разбор заказ
случай «Создан»
то нет
случай «Оплачен»
то да
случай вариант «Отгружен» с «накладная» как накладная
то нет
разбор must name every variant. Remove the last case and the file will not
pass the check:
FLANG_MATCH_NOT_EXHAUSTIVE … разбор «Состояние заказа» не покрывает «Отгружен»
That is the point. A new state that someone forgot about in a rule shows up at
check time, not in production. A chain of если … иначе если over a state
string gives no such guarantee: a forgotten case silently falls into the last
иначе.
A nested record
If an object has compound data — not one value but several fields together — the field gets the type of another record:
модуль «Доставка»
объект «Адрес»
«город»: строка
«индекс»: строка
объект «Заказ»
«номер»: строка
«адрес доставки»: «Адрес»
тотальная функция «Город доставки»
принимает заказ: «Заказ»
возвращает строка
пример «город берётся из вложенной записи»
дано заказ равно запись «Заказ» с «номер» равным "ЗК-1" и «адрес доставки» равным (запись «Адрес» с «город» равным "Казань" и «индекс» равным "420000")
ожидается "Казань"
заказ.«адрес доставки».«город»
In flang ast such a field is { "kind": "named", "name": "Адрес" }. The fields
of the nested record are not expanded there: a form adapter looks up the
address record in the types list itself if it needs its fields.
What an object turns into
A form does not have to be described a second time — its fields are already in
the model. examples/spec/form-schema/schema.mjs turns a record from
flang ast into a form description:
export function flangFormSchema(ast, objectName) {
const records = ast.types.filter((item) => item.kind === 'record');
const record = records.find((item) => item.name === objectName.replace(/^«|»$/gu, ''));
if (!record) {
throw new Error(`в модели нет объекта «${objectName}» (есть: ${records.map((item) => item.name).join(', ')})`);
}
return {
id: `${ast.module}.${record.name}`,
title: record.name,
fields: record.fields.map((field) => {
const control = controlFor(field.type, ast);
const entry = { name: field.name, label: field.name, type: typeName(field.type), control, required: !field.optional };
if (control === 'select') {
entry.options = ast.types.find((item) => item.name === entry.type).variants.map((variant) => variant.name);
}
if (entry.type === 'неотрицательное') entry.min = 0;
return entry;
}),
};
}
A flag becomes a checkbox, неотрицательное a numeric field with min: 0,
and a field with a variant type a choice among the variants. Whether a field is
required comes from optional. Layout, hints and accessibility stay with the
UI component: the model only removes the duplication of names, types and
requiredness.
How types reach TypeScript
flang emit --target ts prints a module where every record is a TypeScript
type. For the invoice line from invoices-table.flang:
export type StrokaSchyota = {
номер: string; контрагент: string; сумма: number; валюта: Valyuta; "срок оплаты": string; просрочен: any
};
Two things are worth knowing in advance. признак and сотых are printed as
any, so TypeScript will not catch a string in place of a flag. The input
boundary does that: the flang_cli.js runner or a check in the service. An
optional field is printed with its own type and without null, and the
record factory puts null into an absent field.
Type and function names in the printed code are transliterations of the
model’s names (StrokaSchyota). Under their original names the functions are
available through $PROGRAM.functions.
Practice in the sandbox
Find the currency type and the «валюта» field of the invoice line record.
In an example, change (вариант «Рубль») to (вариант «Юань») and run the
check: the compiler answers that it expected a variant of the currency type
instead of quietly accepting the value.
The decision type has two variants, and the refusal variant has a причина
field. In the function that returns the refusal reason, remove the
«Разрешено» case and read what the check answers.
The fields «вес» and «расстояние» are different units of measure, but to
the compiler both are just неотрицательное. Think about which separate record
would protect against mixed-up values.
Common mistakes
FLANG_PARSE … поле записывается как '«имя» является типом' — the field
line matches neither «имя»: тип nor имя является типом. A common cause is a
missing word: сумма деньгами instead of «сумма»: деньги.
FLANG_UNKNOWN_NAME … неизвестный тип — the field’s type is declared
nowhere. Declare it in the module (a тип with variants or an объект) or
bring in the module where it is declared.
FLANG_MATCH_NOT_EXHAUSTIVE — a разбор does not name some variant. Add
the case instead of hiding it behind a general branch: this error is the whole
reason a state is made a type.
A name taken by the language. Words like с or состояние are reserved,
and a parameter with such a name gives FLANG_PARSE … это слово занято языком.
Put the name in guillemets or rename it.
Checklist
- I know the built-in types and that
датаandденьгиare a string and a number, not separate checks. - My state is a type with variants, and the match names every variant.
- A field a rule depends on is required or written as a
present/absent variant type rather than with
иногда является. - I can read a record in
flang astand predict its type in the printed TypeScript.