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:
- 使用
Schema.Type工具类型 - 直接在 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 类型:
- 使用
Schema.Encoded工具类型 - 直接在 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 类型:
- 使用
Schema.Context工具类型。 - 在 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 的 Type 与 Encoded 不同时,上面这个技巧会变得更复杂。
示例(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(PersonEncoded 与 Person),并用它们一起重新声明最终的 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.decodeUnknownSync 和 Schema.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.decodeUnknownEither 和 Schema.encodeEither 函数会返回一个 Either:
Either<Type, ParseError>
其中 ParseError 的定义如下(简化版):
interface ParseError {
readonly _tag: "ParseError"
readonly issue: ParseIssue
}
在这个结构中,ParseIssue 表示解析过程中可能出现的错误。它被包装成一个带标签的错误(tagged error),以便使用 Effect.catchTag 更轻松地捕获错误。结果 Either<Type, ParseError> 包含了 schema 所描述的推断数据类型(Type)。解析成功会得到一个带有已解析数据 Type 的 Right 值,而解析失败则会得到一个包含 ParseError 的 Left 值。
默认只返回第一个错误。你可以使用 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)被设置为只显示第一个错误。尽管b和c字段都缺失,但只会报告第一个缺失的字段(b)。
类型守卫
Schema.is 函数提供了一种验证某个值是否符合给定 schema 的方式。它充当一个类型守卫(type guard):接收一个 unknown 类型的值,并判断它是否匹配 schema 中定义的结构和类型约束。
Schema.is 函数的工作方式如下:
-
Schema 定义:定义一个 schema 来描述你期望的数据类型的结构和约束。例如
Schema<Type, Encoded, Context>,其中Type是你要校验的目标类型。 -
创建类型守卫:使用该 schema 创建一个用户定义的类型守卫
(u: unknown) => u is Type。这个函数可以在运行时用来检查某个值是否满足 schema 的要求。
类型 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 不匹配,它会抛出一个详细的错误,因此很适合用于运行时校验。
类型 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.is 和 Schema.asserts 这两个 API,默认行为是严格对待缺失属性,也就是 exact 默认为 true:
示例(用 Schema.is 和 Schema.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 如此。因此,所选的命名约定充当了一套合理的默认值,优先考虑清晰性与易用性,从而便于在多样化的技术环境中进行序列化与反序列化。