# dto | 对象类型定义与验证

-   为对象定义类型与元数据
-   运行时验证对象类型
-   与 TypeScript 类型一致
-   运行时验证比 `zod` 快 1 倍，常见大小对象验证速度在 1 ms 内

## 特点

-   在一处定义类型，可以同时在前端项目和后端项目中使用
-   类型定义是数据（Class/Object）可以编程操纵，同时可以仅作为 type 使用
-   可以自定义元数据，并可以方便的提取(`dtoExtractMeta()`)
-   提供验证器(`dtoVerify()`)
-   不错的性能

```ts
import { dto, dtoVerify } from "fzz"

// 定义对象模型
class DtoUser {
    id = dto.Number()
    name = dto.String()
}

// 在 TypeScript 中直接使用定义对象作为类型
function createUser(user: DtoUser) {
    user.id // number
    user.name // string
}

// 运行时验证
let re = dtoVerify(DtoUser, { id: 123 })
```

## 为什么要用 class

其实用对象也能达到一样的效果，但是作为类型使用时需要用 `typeof` 来提取类型

```ts
let DtoUser = {
    id = dto.Number()
    name = dto.String()
}

let user: typeof DtoUser
```

而使用 `class` 则，既可以作为值来传递，使用时也可以直接作为类型使用，更加方便

```ts
class DtoUser {
    id = dto.Number()
    name = dto.String()
}

let user: DtoUser
```

## 定义

### 基本类型

使用 `class` 和字段赋值类型定义函数来定义对象与对象属性的类型，
这样可以直接在 TypeScript 中使用这个 `class` 作为类型

这样的好处是你永远只需要在一处定义你的模型

```ts
import { dto } from "fzz"

class DtoData {
    myNumber = dto.Number()
    myString = dto.String()
    myBoolean = dto.Boolean()
    myBigInt = dto.BigInt()
    myDate = dto.Date()
}

// 直接作为类型使用
let data: DtoData
function func(arg: DtoData) {}

// 直接作为值传递
let re = dtoVerify(DtoData, {
    myNumber: 123,
    myString: "text",
})
```

-   使用大写（`dto.Number()`）风格的函数名是为了阅读时更容易辨认类型信息

### 数组

直接使用类型定义函数数组来定义数组的类型

```ts
import { dto } from "fzz"

class DtoData {
    numbers = [dto.Number()]
    // 等同于
    numbers = dto.Array(dto.Number())
    // 数组允许多种类型的成员
    numbersOrStrings = [dto.Number(), dto.String()]
}
// 使用模型作为类型：
let data: DtoData = {
    numbers: [3],
    numbersOrStrings: [1, 2, "text"],
}

// 可选属性的对象：
class DtoData2 {
    numbers? = dto.optional.Array(dto.Number())
    numbersOrStrings? = dto.optional.Array([dto.Number(), dto.String()])
}
```

### 嵌套对象

直接使用对象 `{}` 来进行类型定义的嵌套

```ts
import { dto } from "fzz"

class DtoData {
    object = {
        string: dto.String(),
        object: {
            number: dto.Number(),
        },
    }
    emptyObject: {}
}

// 使用模型作为类型：
let data: DtoData = {
    object: {
        string: "text",
        object: {
            number: 1213,
        },
    },
    emptyObject: {},
}
```

### 可选属性

使用在 `dto.optional` 上的类型定义函数来定义可选属性（可为空或为 undefined 的属性）

-   为了在 TypeScript 的类型上也定义为可选，在 `class` 根属性上需要使用 `?=` 来赋值
-   如果子级有可选类型需要使用 `dto.object()` 来包裹下一级模型（如果父级是可选类型就用 `dto.optional()` 来包裹）

```ts
import { dto } from "fzz"

class DtoData {
    // 可选的基本类型属性
    optionalString? = dto.optional.String()
    // 可选的对象
    optionalObject? = dto.optional.Object({
        name: dto.String(),
        // 子级的可选属性
        desc: dto.optional.String(),
    })
    // 子级有可选属性，需要使用 dto.object() 包裹来保证 TypeScript 的类型正确
    object = dto.Object({
        name: dto.String(),
        // 子级的可选属性
        desc: dto.optional.String(),
    })
}
```

### 组合模型

当需要在一个模型中引用另一个模型的时，直接嵌套即刻

如果是 class 定义的 dto 需要用 `new` 实例化欲引用的模型

直接使用 class 名称在效果上是一样的（背后会自动实例化）
但是不用 `new` 的无法让 TypeScript 推断出类型。

```ts
import { dto } from "fzz"

class DtoUser {
    name = dto.String()
    info = new DtoUserInfo()
    info2 = DtoUserInfo // 验证等同于上面一行，但是 TypeScript 无法推断出类型
}

class DtoUserInfo {
    desc = dto.String()
}

let data: DtoUser = {
    name: "name",
    info: {
        desc: "desc....",
    },
}
```

### 验证

类型自带一些验证功能，如字符串的最小长度，数字的最大值等  
在类型定义函数中传入配置对象来定义验证规则

这些验证规则是 meta 的一部分，要自定义 meta 属性的话，
需要注意不要和验证配置冲突

```ts
class DtoUserInfo {
    desc = dto.String({ minLength: 3, maxLength: 100 })
    age = dto.Number({ min: 0, max: 150 })
}
```

### 自定义验证

可以使用定义中的 `is` 属性来定义自定义验证函数，该函数接收一个值，返回一个对象 `{ isOk: boolean; msg?: string }`， `is` 允许数组。

可以使用内置的验证函数 `dtoIs` 中的验证函数

```ts
import { dto, dtoIs } from "fzz"
const { isEmail, in, notIn } = dtoIs

class DtoUserInfo {
    email = dto.String({is:isEmail })
    tags = dto.Array([
        dto.String({ is: in(["tag1", "tag2", "tag3"]) })
    ])
    role = dto.String({ is:  notIn(["guest"])   })
}
```

### 自定义元数据

可以为类型定义设置元数据，并且可以提取信息

```ts
import { dto, dtoExtractMeta } from "fzz"

// 定义元数据
class DtoUserInfo {
    name = dto.String({ index: true })
    desc = dto.String({ index: "text" })
    info = {
        url: dto.String({ index: "text" }),
    }
    users: dto.Array([
        dto.String({ index: true })
    ], { index: false })
}

// 提取元数据
let metaList = dtoExtractMeta(DtoUserInfo)
metaList ==
    [
        { paths: ["name"], meta: { index: true }, type: "string" },
        { paths: ["desc"], meta: { index: "text" }, type: "string" },
        { paths: ["info", "url"], meta: { index: "text" }, type: "string" },
        { paths: ["users" ], meta: { index: false }, type: "Array" },
        { paths: ["users","[*]" ], meta: { index: true }, type: "string" },

    ]
```




### 可选转换

可以使用 `dto.toOptional()`、`dto.toNullable()` 和 `dto.toRequired()` 来动态转换类型的可选与否

```ts
import { dto } from "fzz"

class DtoData {
    name = dto.String()
    age = dto.optional.Number()
}

let DtoDataUpdate = dto.toOptional(DtoData)
let DtoDataCreate = dto.toRequired(DtoData)
let DtoDataNullable = dto.toNullable(DtoData)
```

### 性能

验证数据会消耗额外性能，一个常用的类型验证耗时在 1ms 以内，相比同类库如 zod，本库会快 30% 左右

```
  no schema:
    69 841 ops/s, ±2.20%   | fastest

  @moonvy/dto:
    49 ops/s, ±5.44%       | 99.93% slower

  zod:
    24 ops/s, ±6.08%       | slowest, 99.97% slower
```
