# 属性与类型

模型里的每个字段都是一个**属性声明**。快速上手章已见标量，这一节补全所有类型与它们的边界。

## 标量属性

| 声明 | TS 类型 | 语义 | PostgreSQL |
|---|---|---|---|
| `prop.i8()` / `i16()` / `i32()` / `i64()` | `number`（`i64` 可用 `asString()`） | 有符号整数 | `SMALLINT` ~ `BIGINT` |
| `prop.num(p, s)` | `number` | 定点数（金额等） | `NUMERIC(p, s)` |
| `prop.f32()` / `f64()` | `number` | 浮点 | `FLOAT` / `DOUBLE PRECISION` |
| `prop.str(n)` | `string` | 定长字符串 | `VARCHAR(n)` |
| `prop.text()` | `string` | 长文本 | `TEXT` |
| `prop.bool()` | `boolean` | 布尔 | `BOOLEAN` |
| `prop.dt()` | `Date` | 日期时间 | `TIMESTAMP` |

### i64 的精度问题

`BIGINT` 可超过 `Number.MAX_SAFE_INTEGER`（2⁵³）。读回为 `number` 会丢精度，框架提供两个选择：

```ts
id = prop.i64().asString()   // 以 string 读写 BIGINT；超过 2⁵³ 的 id 用它
id = prop.i64()              // 否则直接用 number 即可
```

`asString()` 自动在数据库 `BIGINT` 与 `string` 之间转换，查询、保存、主键全部一致。

## 可空性

**默认不可空**，`NULL` 需要显式声明：

```ts
store = prop.m2o(BookStore)        // 非空：每条 Book 必须有 store
store = prop.m2o(BookStore).nullable()   // 可空：允许 NULL（表里允许外键为空）
orchidName = prop.str(50).nullable()     // 标量同理
```

可空性会传导到 DTO 类型：视图里的该字段类型是 `T | null`，这也让 TS 的 null 检查贯穿查询与保存。

## 枚举与枚举集合

`prop.enum` 两种写法：枚举常量表（`{ 逻辑名: 值 }`）或值列表（至少两个）：

```ts
gender = prop.enum({             // 逻辑名与存储值分离
    MALE: 'M',
    FEMALE: 'F',
})

tag = prop.enum("URGENT", "NORMAL", "LOW")   // 直接值列表

features = prop.enumSet("READING_ROOM", "AIR_CONDITION")  // 多选：数组
```

- 枚举值只允许字符串/数字，且值必须唯一；
- `enumSet` 在库里落为字符串集合（分隔存储），DTO 类型是数组。

## JSON 与 JSONB（带结构校验）

`prop.json` / `prop.jsonb` 接受一个 **StandardSchemaV1** 结构（zod 等库天然兼容），字段的 TS 类型自动取 schema 的输出类型：

```ts
import { z } from 'zod'

details = prop.json(z.object({
    isbn: z.string(),
    rating: z.number().min(1).max(5),
}))
```

- 结构只描述字段形状与转换，运行时不含反射（「DTO 哲学」讲为什么）；
- `json` 存 `JSON`、`jsonb` 存 `JSONB`（二进制，可索引）。

## 自定义数据类型（进阶）

内置类型不够时用 `prop.scalar` + `ScalarProvider` 定义自定义数据类型（比如把数据库二进制映射成图片对象），完整机制与示例见 **[「查询 API → 自定义数据类型」](/docs/query/custom-type)**。日常开发很少用到。

## 下一步

表与字段都齐了，接下来是重头戏：**关联**——四种关联如何声明、中间表、级联与非对称。
