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

Schema 入门

了解如何定义 schema、提取类型,以及处理解码与编码。

你可以从 effect/Schema 模块导入所需的类型与函数:

示例(命名空间导入)

import * as Schema from "effect/Schema"

示例(具名导入)

import { Schema } from "effect"

定义 Schema

定义 Schema 的一种常见方式就是使用 Struct 构造器。这个构造器让你可以创建一个新的 schema,用来描述一个具有特定属性的对象。 对象中的每个属性都由它自己的 schema 定义,该 schema 规定了数据类型以及任何校验规则。

示例(定义简单的对象 Schema)

这个 Person schema 描述了一个带有 name(string)和 age(number)属性的对象:

import { Schema } from "effect"

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

提取推导出的类型

Type

定义 schema(Schema<Type, Encoded, Context>)之后,你可以通过两种方式提取它推导出的类型 Type

  1. 使用 Schema.Type 工具类型
  2. 直接在 schema 上访问 Type 字段

示例(提取推导出的类型)

import { Schema } from "effect"

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

// 1. Using the Schema.Type utility
type Person = Schema.Schema.Type<typeof Person>

// 2. Accessing the Type field directly
type Person2 = typeof Person.Type

得到的类型如下所示:

type Person = {
  readonly name: string
  readonly age: number
}

另一种方式是使用 interface 关键字提取 Person 类型,在某些情况下这可以提升可读性与性能。

示例(用 interface 提取类型)

import { Schema } from "effect"

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

interface Person extends Schema.Schema.Type<typeof Person> {}

两种方式得到的结果相同,但使用 interface 有性能优势和更好的可读性等好处。

Encoded

Schema<Type, Encoded, Context> 中,Encoded 类型可以与 Type 类型不同,它表示数据被编码时所采用的格式。你可以通过两种方式提取 Encoded 类型:

  1. 使用 Schema.Encoded 工具类型
  2. 直接在 schema 上访问 Encoded 字段

示例(提取编码类型)

import { Schema } from "effect"

const Person = Schema.Struct({
  name: Schema.String,
  // a schema that decodes a string to a number
  age: Schema.NumberFromString,
})

// 1. Using the Schema.Encoded utility
type PersonEncoded = Schema.Schema.Encoded<typeof Person>

// 2. Accessing the Encoded field directly
type PersonEncoded2 = typeof Person.Encoded

得到的类型是:

type PersonEncoded = {
  readonly name: string
  readonly age: string
}

注意,age 在 schema 的 Encoded 类型中是 string 类型,而在 schema 的 Type 类型中是 number 类型。

另一种方式是使用 interface 关键字定义 PersonEncoded 类型,这可以提升可读性与性能。

示例(用 interface 提取编码类型)

import { Schema } from "effect"

const Person = Schema.Struct({
  name: Schema.String,
  // a schema that decodes a string to a number
  age: Schema.NumberFromString,
})

interface PersonEncoded extends Schema.Schema.Encoded<typeof Person> {}

两种方式得到的结果相同,但使用 interface 有性能优势和更好的可读性等好处。

Context

Schema<Type, Encoded, Context> 中,Context 类型表示 schema 执行编码或解码时所需的外部数据或依赖。你可以通过两种方式提取推导出的 Context 类型:

  1. 使用 Schema.Context 工具类型。
  2. 在 schema 上访问 Context 字段。

示例(提取 Context 类型)

import { Schema } from "effect"

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

// 1. Using the Schema.Context utility
type PersonContext = Schema.Schema.Context<typeof Person>

// 2. Accessing the Context field directly
type PersonContext2 = typeof Person.Context

不透明类型的 Schema

在定义 schema 时,你可能想创建一个具有不透明类型的 schema。当你希望隐藏 schema 的内部结构、只暴露该 schema 的类型时,这很有用。

示例(创建不透明 Schema)

要创建具有不透明类型的 schema,可以使用下面这种重新声明 schema 的技巧:

import { Schema } from "effect"

// Define the schema structure
const _Person = Schema.Struct({
  name: Schema.String,
  age: Schema.Number,
})

// Declare the type interface to make it opaque
interface Person extends Schema.Schema.Type<typeof _Person> {}

// Re-declare the schema as opaque
const Person: Schema.Schema<Person> = _Person

另一种方式是使用 Class API(更多细节见 Class API 一节)。

注意,当 schema 的 TypeEncoded 不同时,上面这个技巧会变得更复杂。

示例(Type 与 Encoded 不同的不透明 Schema)

import { Schema } from "effect"

// Define the schema structure, with a field that
// decodes a string to a number
const _Person = Schema.Struct({
  name: Schema.String,
  age: Schema.NumberFromString,
})

// Create the `Type` interface for an opaque schema
interface Person extends Schema.Schema.Type<typeof _Person> {}

// Create the `Encoded` interface for an opaque schema
interface PersonEncoded extends Schema.Schema.Encoded<typeof _Person> {}

// Re-declare the schema with opaque Type and Encoded
const Person: Schema.Schema<Person, PersonEncoded> = _Person

在这个例子中,字段 "age" 在 schema 的 Encoded 类型中是 string 类型,而在 schema 的 Type 类型中是 number 类型。因此,我们需要定义两个 interface(PersonEncodedPerson),并用它们一起重新声明最终的 schema Person

默认的 Readonly 类型

需要注意的是,默认情况下,effect/Schema 导出的多数构造器都会返回 readonly 类型。

示例(Schema 中的 Readonly 类型)

例如,在下面的 Person schema 中:

import { Schema } from "effect"

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

推导出的 Type 是:

{
  readonly name: string;
  readonly age: number;
}

解码

在 TypeScript 中处理未知数据类型时,把未知值解码成已知结构可能很有挑战。好在 effect/Schema 提供了若干函数来帮助完成这一过程。下面来看看如何使用这些函数解码未知值。

API说明
decodeUnknownSync同步解码一个值,解析失败时抛出错误。
decodeUnknownOption解码一个值并返回一个 Option 类型。
decodeUnknownEither解码一个值并返回一个 Either 类型。
decodeUnknownPromise解码一个值并返回一个 Promise
decodeUnknown解码一个值并返回一个 Effect

decodeUnknownSync

当你想要解析一个值,并在解析失败时立即抛出错误时,Schema.decodeUnknownSync 函数很有用。

示例(使用 decodeUnknownSync 立即解码)

import { Schema } from "effect"

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

// Simulate an unknown input
const input: unknown = { name: "Alice", age: 30 }

// Example of valid input matching the schema
console.log(Schema.decodeUnknownSync(Person)(input))
// Output: { name: 'Alice', age: 30 }

// Example of invalid input that does not match the schema
console.log(Schema.decodeUnknownSync(Person)(null))
/*
throws:
ParseError: Expected { readonly name: string; readonly age: number }, actual null
*/

decodeUnknownEither

Schema.decodeUnknownEither 函数让你可以解析一个值,并以 Either 的形式获得结果,它表示成功(Right)或失败(Left)。这种方式让你能够更优雅地处理解析错误,而不必抛出异常。

示例(用 Schema.decodeUnknownEither 处理错误)

import { Schema } from "effect"
import { Either } from "effect"

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

const decode = Schema.decodeUnknownEither(Person)

// Simulate an unknown input
const input: unknown = { name: "Alice", age: 30 }

// Attempt decoding a valid input
const result1 = decode(input)
if (Either.isRight(result1)) {
  console.log(result1.right)
  /*
  Output:
  { name: "Alice", age: 30 }
  */
}

// Simulate decoding an invalid input
const result2 = decode(null)
if (Either.isLeft(result2)) {
  console.log(result2.left)
  /*
  Output:
  {
    _id: 'ParseError',
    message: 'Expected { readonly name: string; readonly age: number }, actual null'
  }
  */
}

decodeUnknown

如果你的 schema 涉及异步转换,那么 Schema.decodeUnknownSyncSchema.decodeUnknownEither 函数就不适用了。 在这种情况下,你应该使用 Schema.decodeUnknown 函数,它返回一个 Effect

示例(处理异步解码)

import { Schema } from "effect"
import { Effect } from "effect"

const PersonId = Schema.Number

const Person = Schema.Struct({
  id: PersonId,
  name: Schema.String,
  age: Schema.Number,
})

const asyncSchema = Schema.transformOrFail(PersonId, Person, {
  strict: true,
  // Decode with simulated async transformation
  decode: (id) =>
    Effect.succeed({ id, name: "name", age: 18 }).pipe(
      Effect.delay("10 millis"),
    ),
  encode: (person) => Effect.succeed(person.id).pipe(Effect.delay("10 millis")),
})

// Attempting to use a synchronous decoder on an async schema
console.log(Schema.decodeUnknownEither(asyncSchema)(1))
/*
Output:
{
  _id: 'Either',
  _tag: 'Left',
  left: {
    _id: 'ParseError',
    message: '(number <-> { readonly id: number; readonly name: string; readonly age: number })\n' +
      '└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work'
  }
}
*/

// Decoding asynchronously with `Schema.decodeUnknown`
Effect.runPromise(Schema.decodeUnknown(asyncSchema)(1)).then(console.log)
/*
Output:
{ id: 1, name: 'name', age: 18 }
*/

在上面的代码中,第一种使用 Schema.decodeUnknownEither 的方式会产生错误,表明该转换无法同步完成。 这是因为 Schema.decodeUnknownEither 并不是为异步操作设计的。 第二种方式使用 Schema.decodeUnknown,它可以正常工作,让你能够处理异步转换并返回预期的结果。

编码

Schema 模块提供了若干 encode* 函数,用于按照 schema 编码数据:

API说明
encodeSync同步编码数据,编码失败时抛出错误。
encodeOption编码数据并返回一个 Option 类型。
encodeEither编码数据并返回表示成功或失败的 Either 类型。
encodePromise编码数据并返回一个 Promise
encode编码数据并返回一个 Effect

示例(使用 Schema.encodeSync 立即编码)

import { Schema } from "effect"

const Person = Schema.Struct({
  // Ensure name is a non-empty string
  name: Schema.NonEmptyString,
  // Allow age to be decoded from a string and encoded to a string
  age: Schema.NumberFromString,
})

// Valid input: encoding succeeds and returns expected types
console.log(Schema.encodeSync(Person)({ name: "Alice", age: 30 }))
// Output: { name: 'Alice', age: '30' }

// Invalid input: encoding fails due to empty name string
console.log(Schema.encodeSync(Person)({ name: "", age: 30 }))
/*
throws:
ParseError: { readonly name: NonEmptyString; readonly age: NumberFromString }
└─ ["name"]
   └─ NonEmptyString
      └─ Predicate refinement failure
         └─ Expected a non empty string, actual ""
*/

注意,在编码过程中,数字值 30 被转换成了字符串 "30"

处理不支持的编码

在某些情况下,为某个 schema 支持编码可能并不可行。虽然通常建议把 schema 定义为同时支持解码与编码,但有时对某种特定类型进行编码既不受支持、也没有必要。在这些情况下,可以用 Forbidden issue 来表明某些值无法进行编码。

示例(用 Forbidden 表示不支持的编码)

下面是一个在解码过程中永不失败的转换示例。它返回一个 Either,其中包含的要么是解码后的值,要么是原始输入。对于编码而言,不支持它是合理的,并用 Forbidden 作为结果。

import { Either, ParseResult, Schema } from "effect"

// Define a schema that safely decodes to Either type
export const SafeDecode = <A, I>(self: Schema.Schema<A, I, never>) => {
  const decodeUnknownEither = Schema.decodeUnknownEither(self)
  return Schema.transformOrFail(
    Schema.Unknown,
    Schema.EitherFromSelf({
      left: Schema.Unknown,
      right: Schema.typeSchema(self),
    }),
    {
      strict: true,
      // Decode: map a failed result to the input as Left,
      // successful result as Right
      decode: (input) =>
        ParseResult.succeed(
          Either.mapLeft(decodeUnknownEither(input), () => input),
        ),
      // Encode: only support encoding Right values,
      // Left values raise Forbidden error
      encode: (actual, _, ast) =>
        Either.match(actual, {
          onLeft: () =>
            ParseResult.fail(
              new ParseResult.Forbidden(ast, actual, "cannot encode a Left"),
            ),
          // Successfully encode a Right value
          onRight: ParseResult.succeed,
        }),
    },
  )
}

说明

  • 解码SafeDecode 函数确保解码永不失败。它把解码后的值包装进一个 Either:解码成功得到 Right,解码失败则得到包含原始输入的 Left
  • 编码:编码过程使用 Forbidden 错误来表明不支持对 Left 值进行编码。只有 Right 值能够被成功编码。

ParseError

Schema.decodeUnknownEitherSchema.encodeEither 函数会返回一个 Either

Either<Type, ParseError>

其中 ParseError 的定义如下(简化版):

interface ParseError {
  readonly _tag: "ParseError"
  readonly issue: ParseIssue
}

在这个结构中,ParseIssue 表示解析过程中可能出现的错误。它被包装成一个带标签的错误(tagged error),以便使用 Effect.catchTag 更轻松地捕获错误。结果 Either<Type, ParseError> 包含了 schema 所描述的推断数据类型(Type)。解析成功会得到一个带有已解析数据 TypeRight 值,而解析失败则会得到一个包含 ParseErrorLeft 值。

Returning All Errors

默认只返回第一个错误。你可以使用 errors 选项来接收全部错误。

解析选项

下面这些选项可以同时控制解码和编码的行为。

管理多余属性

默认情况下,解析一个值时,schema 中未定义的任何属性都会从输出中移除。这能确保解析出的数据严格符合预期的结构。

如果你想检测并处理意料之外的属性,可以使用 onExcessProperty 选项(默认值为 "ignore"),它允许你针对多余属性抛出错误。当你需要校验并捕获未预料到的属性时,这会很有帮助。

示例(把 onExcessProperty 设为 "error"

import { Schema } from "effect"

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

// Excess properties are ignored by default
console.log(
  Schema.decodeUnknownSync(Person)({
    name: "Bob",
    age: 40,
    email: "bob@example.com", // Ignored
  }),
)
/*
Output:
{ name: 'Bob', age: 40 }
*/

// With `onExcessProperty` set to "error",
// an error is thrown for excess properties
Schema.decodeUnknownSync(Person)(
  {
    name: "Bob",
    age: 40,
    email: "bob@example.com", // Will raise an error
  },
  { onExcessProperty: "error" },
)
/*
throws
ParseError: { readonly name: string; readonly age: number }
└─ ["email"]
   └─ is unexpected, expected: "name" | "age"
*/

如果想保留额外的属性,请把 onExcessProperty 设为 "preserve"

示例(把 onExcessProperty 设为 "preserve"

import { Schema } from "effect"

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

// Excess properties are preserved in the output
console.log(
  Schema.decodeUnknownSync(Person)(
    {
      name: "Bob",
      age: 40,
      email: "bob@example.com",
    },
    { onExcessProperty: "preserve" },
  ),
)
/*
{ email: 'bob@example.com', name: 'Bob', age: 40 }
*/

接收全部错误

errors 选项让你能够获取解析过程中遇到的所有错误。默认只返回第一个错误。把 errors 设为 "all" 会提供完整的错误反馈,这在调试或给出详细的校验反馈时很有用。

示例(把 errors 设为 "all"

import { Schema } from "effect"

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

// Attempt to parse with multiple issues in the input data
Schema.decodeUnknownSync(Person)(
  {
    name: "Bob",
    age: "abc",
    email: "bob@example.com",
  },
  { errors: "all", onExcessProperty: "error" },
)
/*
throws
ParseError: { readonly name: string; readonly age: number }
├─ ["email"]
│  └─ is unexpected, expected: "name" | "age"
└─ ["age"]
   └─ Expected number, actual "abc"
*/

管理属性顺序

propertyOrder 选项可以控制输出中对象字段的顺序。当键的顺序对下游消费流程很重要,或者保持输入顺序能提升可读性和易用性时,这个特性尤其有用。

默认情况下,propertyOrder 选项被设为 "none"。这意味着由内部系统决定键的顺序,以优化解析速度。在此模式下,键的顺序不应被视为稳定的,建议不要依赖键的顺序,因为它可能在未来更新中发生变化。

propertyOrder 设为 "original" 可以确保在解码/编码过程中,键按照它们在输入中出现的顺序排列。

示例(同步解码)

import { Schema } from "effect"

const schema = Schema.Struct({
  a: Schema.Number,
  b: Schema.Literal("b"),
  c: Schema.Number,
})

// Default decoding, where property order is system-defined
console.log(Schema.decodeUnknownSync(schema)({ b: "b", c: 2, a: 1 }))
// Output may vary: { a: 1, b: 'b', c: 2 }

// Decoding while preserving input order
console.log(
  Schema.decodeUnknownSync(schema)(
    { b: "b", c: 2, a: 1 },
    { propertyOrder: "original" },
  ),
)
// Output preserves input order: { b: 'b', c: 2, a: 1 }

示例(异步解码)

import type { Duration } from "effect"
import { Effect, ParseResult, Schema } from "effect"

// Helper function to simulate an async operation in schema
const effectify = (duration: Duration.DurationInput) =>
  Schema.Number.pipe(
    Schema.transformOrFail(Schema.Number, {
      strict: true,
      decode: (x) =>
        Effect.sleep(duration).pipe(Effect.andThen(ParseResult.succeed(x))),
      encode: ParseResult.succeed,
    }),
  )

// Define a structure with asynchronous behavior in each field
const schema = Schema.Struct({
  a: effectify("200 millis"),
  b: effectify("300 millis"),
  c: effectify("100 millis"),
}).annotations({ concurrency: 3 })

// Default decoding, where property order is system-defined
Schema.decode(schema)({ a: 1, b: 2, c: 3 })
  .pipe(Effect.runPromise)
  .then(console.log)
// Output decided internally: { c: 3, a: 1, b: 2 }

// Decoding while preserving input order
Schema.decode(schema)({ a: 1, b: 2, c: 3 }, { propertyOrder: "original" })
  .pipe(Effect.runPromise)
  .then(console.log)
// Output preserving input order: { a: 1, b: 2, c: 3 }

在 schema 层级自定义解析行为

parseOptions 注解(annotation)允许你在不同的 schema 层级自定义解析行为,让你能够把独特的解析设置应用到结构体中的嵌套 schema。在某个 schema 内部定义的选项会覆盖父层级的设置,并应用到所有嵌套的 schema。

示例(用 parseOptions 自定义错误处理)

import { Schema } from "effect"
import { Either } from "effect"

const schema = Schema.Struct({
  a: Schema.Struct({
    b: Schema.String,
    c: Schema.String,
  }).annotations({
    title: "first error only",
    // Limit errors to the first in this sub-schema
    parseOptions: { errors: "first" },
  }),
  d: Schema.String,
}).annotations({
  title: "all errors",
  // Capture all errors for the main schema
  parseOptions: { errors: "all" },
})

// Decode input with custom error-handling behavior
const result = Schema.decodeUnknownEither(schema)(
  { a: {} },
  { errors: "first" },
)
if (Either.isLeft(result)) {
  console.log(result.left.message)
}
/*
all errors
├─ ["a"]
│  └─ first error only
│     └─ ["b"]
│        └─ is missing
└─ ["d"]
   └─ is missing
*/

输出详解:

在这个例子中:

  • 主 schema 被配置为显示所有错误。因此,你会看到与 d 字段相关的错误(因为它缺失),以及来自 a 子 schema 的错误。
  • 子 schema(a)被设置为只显示第一个错误。尽管 bc 字段都缺失,但只会报告第一个缺失的字段(b)。

类型守卫

Schema.is 函数提供了一种验证某个值是否符合给定 schema 的方式。它充当一个类型守卫(type guard):接收一个 unknown 类型的值,并判断它是否匹配 schema 中定义的结构和类型约束。

Schema.is 函数的工作方式如下:

  1. Schema 定义:定义一个 schema 来描述你期望的数据类型的结构和约束。例如 Schema<Type, Encoded, Context>,其中 Type 是你要校验的目标类型。

  2. 创建类型守卫:使用该 schema 创建一个用户定义的类型守卫 (u: unknown) => u is Type。这个函数可以在运行时用来检查某个值是否满足 schema 的要求。

Role of the Encoded Type in Type Guards

类型 Encoded 在 schema 变换中经常被用到,但它不影响类型守卫的创建。其主要目的是确保输入与期望的类型 Type 匹配。

示例(创建并使用类型守卫)

import { Schema } from "effect"

// Define a schema for a Person object
const Person = Schema.Struct({
  name: Schema.String,
  age: Schema.Number,
})

// Generate a type guard from the schema
const isPerson = Schema.is(Person)

// Test the type guard with various inputs
console.log(isPerson({ name: "Alice", age: 30 }))
// Output: true

console.log(isPerson(null))
// Output: false

console.log(isPerson({}))
// Output: false

生成的 isPerson 函数具有以下签名:

const isPerson: (
  u: unknown,
  overrideOptions?: number | ParseOptions,
) => u is {
  readonly name: string
  readonly age: number
}

断言

类型守卫验证的是某个值是否符合特定类型,而 Schema.asserts 函数则更进一步:它断言输入匹配 schema 类型 Type(来自 Schema<Type, Encoded, Context>)。如果输入与 schema 不匹配,它会抛出一个详细的错误,因此很适合用于运行时校验。

Role of the Encoded Type in Assertions

类型 Encoded 在 schema 变换中经常被用到,但它不影响断言的创建。其主要目的是确保输入与期望的类型 Type 匹配。

示例(创建并使用断言)

import { Schema } from "effect"

// Define a schema for a Person object
const Person = Schema.Struct({
  name: Schema.String,
  age: Schema.Number,
})

// Generate an assertion function from the schema
const assertsPerson: Schema.Schema.ToAsserts<typeof Person> =
  Schema.asserts(Person)

try {
  // Attempt to assert that the input matches the Person schema
  assertsPerson({ name: "Alice", age: "30" })
} catch (e) {
  console.error("The input does not match the schema:")
  console.error(e)
}
/*
throws:
The input does not match the schema:
{
  _id: 'ParseError',
  message: '{ readonly name: string; readonly age: number }\n' +
    '└─ ["age"]\n' +
    '   └─ Expected number, actual "30"'
}
*/

// This input matches the schema and will not throw an error
assertsPerson({ name: "Alice", age: 30 })

由 schema 生成的 assertsPerson 函数具有以下签名:

const assertsPerson: (
  input: unknown,
  overrideOptions?: number | ParseOptions,
) => asserts input is {
  readonly name: string
  readonly age: number
}

管理缺失属性

解码时,理解缺失属性是如何被处理的很重要。默认情况下,如果输入中不存在某个属性,它会被视为以 undefined 值存在。

示例(缺失属性的默认行为)

import { Schema } from "effect"

const schema = Schema.Struct({ a: Schema.Unknown })
const input = {}

console.log(Schema.decodeUnknownSync(schema)(input))
// Output: { a: undefined }

在这个例子中,尽管键 "a" 不存在于输入中,默认情况下它会被当作 { a: undefined }

如果你需要校验逻辑区分真正缺失的属性和显式设为 undefined 的属性,可以启用 exact 选项。

示例(设置 exact: true 来区分缺失属性)

import { Schema } from "effect"

const schema = Schema.Struct({ a: Schema.Unknown })
const input = {}

console.log(Schema.decodeUnknownSync(schema)(input, { exact: true }))
/*
throws
ParseError: { readonly a: unknown }
└─ ["a"]
   └─ is missing
*/

不过,对于 Schema.isSchema.asserts 这两个 API,默认行为是严格对待缺失属性,也就是 exact 默认为 true

示例(用 Schema.isSchema.asserts 严格处理缺失属性)

import type { SchemaAST } from "effect"
import { Schema } from "effect"

const schema = Schema.Struct({ a: Schema.Unknown })
const input = {}

console.log(Schema.is(schema)(input))
// Output: false

console.log(Schema.is(schema)(input, { exact: false }))
// Output: true

const asserts: (
  u: unknown,
  overrideOptions?: SchemaAST.ParseOptions,
) => asserts u is {
  readonly a: unknown
} = Schema.asserts(schema)

try {
  asserts(input)
  console.log("asserts passed")
} catch (e: any) {
  console.error("asserts failed")
  console.error(e.message)
}
/*
Output:
asserts failed
{ readonly a: unknown }
└─ ["a"]
  └─ is missing
*/

try {
  asserts(input, { exact: false })
  console.log("asserts passed")
} catch (e: any) {
  console.error("asserts failed")
  console.error(e.message)
}
// Output: asserts passed

命名约定

effect/Schema 中的命名约定力求直白、合乎逻辑,首要考虑的是与 JSON 序列化的兼容性。这种做法简化了对 schema 的理解与使用,尤其对那些正在集成 Web 技术的开发者而言更是如此——在 Web 技术中,JSON 是标准的数据交换格式。

命名策略概览

与 JSON 兼容的类型

那些天然就能序列化为 JSON 兼容格式的 schema,会直接以其数据类型来命名。

例如:

  • Schema.Date:把 JavaScript 的 Date 对象序列化为 ISO 格式的字符串,这是 JSON 中表示日期的典型做法。
  • Schema.Number:直接使用,因为它与 JSON 的 number 类型精确对应,无需任何特殊转换即可保持 JSON 兼容。

与 JSON 不兼容的类型

当处理的类型在 JSON 中没有直接对应的表示时,命名策略会加入额外的细节来指明所需的转换。这有助于对 schema 的行为建立清晰的预期:

例如:

  • Schema.DateFromSelf:表明该 schema 处理的是 Date 对象,而这类对象本身并不能被 JSON 直接序列化。
  • Schema.NumberFromString:这一命名暗示该 schema 处理的是最初以字符串形式表示的数字,强调在解码时从字符串到数字的转换。

这些 schema 的首要目标是确保领域对象能够方便地序列化(“encoded”)与反序列化(“decoded”),以便通过网络连接传输,从而便于它们在同一应用的不同部分之间、或在不同应用之间传递。

理由

尽管 JSON 的普遍性使其成为命名时的首要考量,这些约定同样兼顾了其他传输类型的序列化需求。例如,把 Date 转换为字符串对各种通信协议都普遍有用,并非只对 JSON 如此。因此,所选的命名约定充当了一套合理的默认值,优先考虑清晰性与易用性,从而便于在多样化的技术环境中进行序列化与反序列化。