已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

默认构造器

借助针对 Struct、Record、Filter 与品牌类型的默认构造器,轻松创建符合 schema 的值,并支持校验、默认值与惰性求值等选项。

在处理数据结构时,能够以最小的代价创建符合某个 schema 的值往往很有帮助。 为此,Schema 模块为多种 schema 类型提供了默认构造器,涵盖 StructsRecordsfiltersbrands

Constructor Scope

与类型为 Schema<A, I, R> 的 schema 相关联的默认构造器,只作用于解码后的类型A),而不是编码后的类型(I)。

  • A(解码类型):这是解码与校验之后产生的类型。构造器创建的就是该类型的值。
  • I(编码类型):这是解码原始输入时所期望的类型。构造器不接受该类型。

当处理会转换数据的 schema 时,这一区别很重要。例如,如果某个 schema 把字符串解码为数字,那么默认构造器只接受数字,而不接受字符串。

默认构造器是不安全的,这意味着当输入不符合 schema 时,它们会抛出错误。 如果你需要一个更安全的替代方案,可以考虑使用 Schema.validateEither,它返回一个表示成功或失败的结果,而不是抛出错误。

示例(使用 Refinement 的默认构造器)

import { Schema } from "effect"

const schema = Schema.NumberFromString.pipe(Schema.between(1, 10))

// The constructor only accepts numbers
console.log(schema.make(5))
// Output: 5

// This will throw an error because the number is outside the valid range
console.log(schema.make(20))
/*
throws:
ParseError: between(1, 10)
└─ Predicate refinement failure
   └─ Expected a number between 1 and 10, actual 20
*/

Structs

Struct schema 允许你定义具有特定字段与约束的对象。make 函数可用于创建某个 struct schema 的实例。

示例(创建 Struct 实例)

import { Schema } from "effect"

const Struct = Schema.Struct({
  name: Schema.NonEmptyString,
})

// Successful creation
Struct.make({ name: "a" })

// This will throw an error because the name is empty
Struct.make({ name: "" })
/*
throws
ParseError: { readonly name: NonEmptyString }
└─ ["name"]
   └─ NonEmptyString
      └─ Predicate refinement failure
         └─ Expected NonEmptyString, actual ""
*/

在某些情况下,你可能需要绕过校验。虽然在大多数场景下并不推荐,但 make 提供了一个禁用校验的选项。

示例(绕过校验)

import { Schema } from "effect"

const Struct = Schema.Struct({
  name: Schema.NonEmptyString,
})

// Bypass validation during instantiation
Struct.make({ name: "" }, true)

// Or use the `disableValidation` option explicitly
Struct.make({ name: "" }, { disableValidation: true })

Records

Record schema 允许你定义键值映射,其中的键与值都必须满足特定条件。

示例(创建 Record 实例)

import { Schema } from "effect"

const Record = Schema.Record({
  key: Schema.String,
  value: Schema.NonEmptyString,
})

// Successful creation
Record.make({ a: "a", b: "b" })

// This will throw an error because 'b' is empty
Record.make({ a: "a", b: "" })
/*
throws
ParseError: { readonly [x: string]: NonEmptyString }
└─ ["b"]
   └─ NonEmptyString
      └─ Predicate refinement failure
         └─ Expected NonEmptyString, actual ""
*/

// Bypasses validation
Record.make({ a: "a", b: "" }, { disableValidation: true })

Filters

Filter 允许你为单个值定义约束。

示例(使用 Filter 强制取值范围)

import { Schema } from "effect"

const MyNumber = Schema.Number.pipe(Schema.between(1, 10))

// Successful creation
const n = MyNumber.make(5)

// This will throw an error because the number is outside the valid range
MyNumber.make(20)
/*
throws
ParseError: a number between 1 and 10
└─ Predicate refinement failure
   └─ Expected a number between 1 and 10, actual 20
*/

// Bypasses validation
MyNumber.make(20, { disableValidation: true })

Branded Types

Branded schema 会为值添加元数据,从而赋予它更具体的类型,同时仍保留其原始类型。

示例(创建 Branded 值)

import { Schema } from "effect"

const BrandedNumberSchema = Schema.Number.pipe(
  Schema.between(1, 10),
  Schema.brand("MyNumber"),
)

// Successful creation
const n = BrandedNumberSchema.make(5)

// This will throw an error because the number is outside the valid range
BrandedNumberSchema.make(20)
/*
throws
ParseError: a number between 1 and 10 & Brand<"MyNumber">
└─ Predicate refinement failure
   └─ Expected a number between 1 and 10 & Brand<"MyNumber">, actual 20
*/

// Bypasses validation
BrandedNumberSchema.make(20, { disableValidation: true })

在使用默认构造器时,理解它们产出的值的类型会很有帮助。

例如,在 BrandedNumberSchema 示例中,构造器的返回类型是 number & Brand<"MyNumber">。这表明得到的值是一个带有额外 branding 信息 "MyNumber"number

这种行为与 Filter 示例形成对比:后者的返回类型就是 number。Branding 会增加一层额外的类型信息,有助于更有效地识别和处理你的数据。

Error Handling in Constructors

默认构造器被认为”不安全”,因为当输入不符合 schema 时它们会抛出错误。该错误包含对出错原因的详细描述。默认构造器的用意在于提供一种直接创建合法值的方式,例如用于测试或配置——在这些场景中,无效输入本就被视为异常情况。

如果你需要一个不抛出错误、而是返回表示成功或失败的结果的”安全”构造器,可以使用 Schema.validateEither

示例(使用 Schema.validateEither 进行安全校验)

import { Schema } from "effect"

const schema = Schema.NumberFromString.pipe(Schema.between(1, 10))

// Create a safe constructor that validates an unknown input
const safeMake = Schema.validateEither(schema)

// Valid input returns a Right value
console.log(safeMake(5))
/*
Output:
{ _id: 'Either', _tag: 'Right', right: 5 }
*/

// Invalid input returns a Left value with detailed error information
console.log(safeMake(20))
/*
Output:
{
  _id: 'Either',
  _tag: 'Left',
  left: {
    _id: 'ParseError',
    message: 'between(1, 10)\n' +
      '└─ Predicate refinement failure\n' +
      '   └─ Expected a number between 1 and 10, actual 20'
  }
}
*/

// This will throw an error because it's unsafe
schema.make(20)
/*
throws:
ParseError: between(1, 10)
└─ Predicate refinement failure
   └─ Expected a number between 1 and 10, actual 20
*/

Setting Default Values

在创建对象时,你可能希望为某些字段指定默认值,以简化对象的构造。Schema.withConstructorDefault 函数让你可以处理默认值,从而使这些字段在默认构造器中变为可选。

示例(包含必填字段的 Struct)

在这个示例中,创建新实例时所有字段都是必填的。

import { Schema } from "effect"

const Person = Schema.Struct({
  name: Schema.NonEmptyString,
  age: Schema.Number,
})

// Both name and age must be provided
console.log(Person.make({ name: "John", age: 30 }))
/*
Output: { name: 'John', age: 30 }
*/

示例(带默认值的 Struct)

这里,age 字段是可选的,因为它有默认值 0

import { Schema } from "effect"

const Person = Schema.Struct({
  name: Schema.NonEmptyString,
  age: Schema.Number.pipe(
    Schema.propertySignature,
    Schema.withConstructorDefault(() => 0),
  ),
})

// The age field is optional and defaults to 0
console.log(Person.make({ name: "John" }))
/*
Output:
{ name: 'John', age: 0 }
*/

console.log(Person.make({ name: "John", age: 30 }))
/*
Output:
{ name: 'John', age: 30 }
*/

Nested Structs and Shallow Defaults

schema 中的默认值是浅层的,这意味着嵌套 struct 中定义的默认值不会自动传播到顶层的构造器。

示例(嵌套 Struct 中的浅层默认值)

import { Schema } from "effect"

const Config = Schema.Struct({
  // Define a nested struct with a default value
  web: Schema.Struct({
    application_url: Schema.String.pipe(
      Schema.propertySignature,
      Schema.withConstructorDefault(() => "http://localhost"),
    ),
    application_port: Schema.Number,
  }),
})

// This will cause a type error because `application_url`
// is missing in the nested struct
// @errors: 2741
Config.make({ web: { application_port: 3000 } })

之所以会出现这种行为,是因为 Schema 接口并不包含用于从嵌套 struct 中携带默认构造器类型的类型参数。

要绕过这一限制,可以把嵌套 struct 的构造器提取出来,并直接应用在它的字段上。这样就能确保嵌套的默认值得到遵守。

示例(使用嵌套 Struct 的构造器)

import { Schema } from "effect"

const Config = Schema.Struct({
  web: Schema.Struct({
    application_url: Schema.String.pipe(
      Schema.propertySignature,
      Schema.withConstructorDefault(() => "http://localhost"),
    ),
    application_port: Schema.Number,
  }),
})

// Extract the nested struct constructor
const { web: Web } = Config.fields

// Use the constructor for the nested struct
console.log(Config.make({ web: Web.make({ application_port: 3000 }) }))
/*
Output:
{
  web: {
    application_url: 'http://localhost',
    application_port: 3000
  }
}
*/

Lazy Evaluation of Defaults

默认值是惰性求值的:每次调用构造器时,都会生成一个新的默认值实例:

示例(默认值的惰性求值)

在这个示例中,timestamp 字段会为每个实例生成一个新值。

import { Schema } from "effect"

const Person = Schema.Struct({
  name: Schema.NonEmptyString,
  age: Schema.Number.pipe(
    Schema.propertySignature,
    Schema.withConstructorDefault(() => 0),
  ),
  timestamp: Schema.Number.pipe(
    Schema.propertySignature,
    Schema.withConstructorDefault(() => new Date().getTime()),
  ),
})

console.log(Person.make({ name: "name1" }))
/*
Example Output:
{ age: 0, timestamp: 1714232909221, name: 'name1' }
*/

console.log(Person.make({ name: "name2" }))
/*
Example Output:
{ age: 0, timestamp: 1714232909227, name: 'name2' }
*/

Reusing Defaults Across Schemas

默认值还是”可移植的”:如果你在另一个 schema 中复用同一个属性签名,该默认值会被一并带过去:

示例(在另一个 Schema 中复用默认值)

import { Schema } from "effect"

const Person = Schema.Struct({
  name: Schema.NonEmptyString,
  age: Schema.Number.pipe(
    Schema.propertySignature,
    Schema.withConstructorDefault(() => 0),
  ),
  timestamp: Schema.Number.pipe(
    Schema.propertySignature,
    Schema.withConstructorDefault(() => new Date().getTime()),
  ),
})

const AnotherSchema = Schema.Struct({
  foo: Schema.String,
  age: Person.fields.age,
})

console.log(AnotherSchema.make({ foo: "bar" }))
/*
Output:
{ foo: 'bar', age: 0 }
*/

Using Defaults in Classes

在使用 Class API 时也可以应用默认值,从而确保基于 Class 的 schema 之间保持一致。

示例(Class 中的默认值)

import { Schema } from "effect"

class Person extends Schema.Class<Person>("Person")({
  name: Schema.NonEmptyString,
  age: Schema.Number.pipe(
    Schema.propertySignature,
    Schema.withConstructorDefault(() => 0),
  ),
  timestamp: Schema.Number.pipe(
    Schema.propertySignature,
    Schema.withConstructorDefault(() => new Date().getTime()),
  ),
}) {}

console.log(new Person({ name: "name1" }))
/*
Example Output:
Person { age: 0, timestamp: 1714400867208, name: 'name1' }
*/

console.log(new Person({ name: "name2" }))
/*
Example Output:
Person { age: 0, timestamp: 1714400867215, name: 'name2' }
*/