高级用法
学习定义和扩展数据 schema 的高级技巧,包括递归类型与互递归类型、可选字段、品牌类型以及 schema 变换。
声明新的数据类型
原始数据类型
要为一种不透明的、非泛型的数据类型声明 schema,可以把 Schema.declare 与类型守卫配合使用。下面的示例以 File 展示了这种底层模式。
Effect 已经提供了 Schema.File;下面这里的声明仅用于说明。
示例(为 File 声明 Schema)
import { Schema } from "effect"
// Declare a schema for the File type using a type guard
const FileSchema = Schema.declare(
(input: unknown): input is File => input instanceof File,
)
const decode = Schema.decodeUnknownSync(FileSchema)
// Decoding a valid File object
console.log(decode(new File([], "")))
/*
Output:
File { size: 0, type: '', name: '', lastModified: 1724774163056 }
*/
// Decoding an invalid input
decode(null)
/*
throws
SchemaError: Expected <Declaration>
*/
像 identifier、description 这样的注解有助于改进报错信息,
也让 schema 具备自解释性。
你可以添加 identifier、title 和 description 注解,让这个声明更容易被人和 schema 解释器理解。identifier 和 title 还能改进默认的期望值消息。
- Identifier:schema 的唯一名称
- Title:简短、描述性的标题
- Description:对 schema 用途的详细说明
示例(声明带注解的 Schema)
import { Schema } from "effect"
// Declare a schema for the File type with additional annotations
const FileSchema = Schema.declare(
(input: unknown): input is File => input instanceof File,
{
// A unique identifier for the schema
identifier: "File",
// Detailed description of the schema
description: "The `File` type in JavaScript",
},
)
const decode = Schema.decodeUnknownSync(FileSchema)
// Decoding a valid File object
console.log(decode(new File([], "")))
/*
Output:
File { size: 0, type: '', name: '', lastModified: 1724774163056 }
*/
// Decoding an invalid input
decode(null)
/*
throws
SchemaError: Expected File
*/
类型构造器
类型构造器是接收一个或多个类型作为参数、并返回一个新类型的泛型类型。要为类型构造器定义 schema,可以使用 Schema.declare 函数。
示例(为 ReadonlySet<T> 声明 Schema)
import {
Effect,
Schema,
SchemaIssue,
SchemaParser,
SchemaTransformation,
} from "effect"
export const MyReadonlySet = <S extends Schema.Constraint>(
// Schema for the elements of the Set
item: S,
) =>
Schema.declareConstructor<
ReadonlySet<S["Type"]>,
ReadonlySet<S["Encoded"]>
>()(
// Store the schema for the Set's elements
[item],
// Decoding function
([item]) =>
(input, ast, options) => {
if (input instanceof Set) {
// Decode each element in the Set
return Effect.map(
SchemaParser.decodeUnknownEffect(Schema.Array(item))(
Array.from(input.values()),
options,
),
// Return a ReadonlySet containing the decoded elements
(values): ReadonlySet<S["Type"]> => new Set(values),
)
}
// Handle invalid input
return Effect.fail(new SchemaIssue.InvalidType(ast))
},
{
expected: "ReadonlySet",
// Define the encoding side by linking back to an Array schema
toCodec: ([item]) =>
Schema.link<ReadonlySet<S["Encoded"]>>()(
Schema.Array(item),
SchemaTransformation.transform({
// Decode an array into a ReadonlySet
decode: (values): ReadonlySet<S["Encoded"]> => new Set(values),
// Encode a ReadonlySet back into an array
encode: (set) => Array.from(set.values()),
}),
),
},
)
// Define a schema for a ReadonlySet of numbers
const setOfNumbers = MyReadonlySet(Schema.FiniteFromString)
const decode = Schema.decodeUnknownSync(setOfNumbers)
console.log(decode(new Set(["1", "2", "3"]))) // Set(3) { 1, 2, 3 }
// Decode an invalid input
decode(null)
/*
throws
SchemaError: Expected ReadonlySet
*/
// Decode a Set with an invalid element
decode(new Set(["1", null, "3"]))
/*
throws
SchemaError: Expected string
at [1]
*/
declareConstructor 返回的解析器本身是 effectful 的,但最终生成的 codec
只会依赖其类型参数 schema 已经需要的解码与编码服务。
添加解释器注解
定义一种新的数据类型时,诸如 Arbitrary 或 Formatter 这样的 schema 解释器可能不知道如何处理这个新类型。 这会导致错误,因为解释器可能缺少生成实例或产出可读输出所需的信息:
示例(在没有必需注解的情况下尝试生成 Arbitrary 值)
import { Schema } from "effect"
// Define a schema for the File type
const FileSchema = Schema.declare(
(input: unknown): input is File => input instanceof File,
{
identifier: "File",
},
)
// Try creating an Arbitrary instance for the schema
const arb = Schema.toArbitrary(FileSchema)
/*
throws:
Error: Missing annotation
details: Generating an Arbitrary for this schema requires an "arbitrary" annotation
schema (Declaration): File
*/
在上面的示例中,为 FileSchema 生成 arbitrary 值会失败,因为解释器缺少必需的注解。要解决这个问题,请提供用于生成 arbitrary 数据的注解:
示例(为自定义的 File Schema 添加 Arbitrary 注解)
import { Schema } from "effect"
import { FastCheck } from "effect/testing"
const FileSchema = Schema.declare(
(input: unknown): input is File => input instanceof File,
{
identifier: "File",
// Provide a function to generate random File instances
toArbitrary: () => (fc) =>
fc
.tuple(fc.string(), fc.string())
.map(([content, path]) => new File([content], path)),
},
)
// Create an Arbitrary instance for the schema
const arb = Schema.toArbitrary(FileSchema)
// Generate sample files using the Arbitrary instance
const files = FastCheck.sample(arb, 2)
console.log(files)
/*
Example Output:
[
File { size: 5, type: '', name: 'C', lastModified: 1706435571176 },
File { size: 1, type: '', name: '98Ggmc', lastModified: 1706435571176 }
]
*/
关于如何为 Arbitrary 解释器添加注解的更多细节,请参阅 Arbitrary 文档。
品牌类型
TypeScript 的类型系统是结构化的,这意味着任何两个在结构上等价的类型都会被视为同一个类型。 当语义上不同的类型被当作同一个类型处理时,这就会带来问题。
示例(结构化类型带来的问题)
type UserId = string
type Username = string
declare const getUser: (id: UserId) => object
const myUsername: Username = "gcanti"
getUser(myUsername) // This erroneously works
在上面的示例中,UserId 和 Username 都是同一个类型 string 的别名。这意味着 getUser 函数会误把一个 Username 当作合法的 UserId 接受,从而带来 bug 和错误。
为了避免这种情况,Effect 引入了品牌类型(branded types)。这类类型会给一个类型附加一个唯一标识(也就是 “brand”),让你能够区分结构相似但语义不同的类型。
示例(定义品牌类型)
import { Brand } from "effect"
type UserId = string & Brand.Brand<"UserId">
type Username = string
declare const getUser: (id: UserId) => object
const myUsername: Username = "gcanti"
// @errors: 2345
getUser(myUsername)
通过把 UserId 定义为品牌类型,getUser 函数就只能接受 UserId 类型的值,而不能接受普通字符串或其他与字符串兼容的类型。这有助于避免因误把错误类型的值传给函数而引发的 bug。
为品牌类型定义 schema 有两种方式,取决于你是:
- 想从零开始定义 schema
- 已经通过
effect/Brand定义了品牌类型,想复用它来定义 schema
从零定义品牌 schema
要从零为品牌类型定义 schema,请使用 Schema.brand 函数。
示例(为品牌类型创建 schema)
import { Schema } from "effect"
const UserId = Schema.String.pipe(Schema.brand("UserId"))
// string & Brand<"UserId">
type UserId = typeof UserId.Type
复用已有的品牌构造器
如果你已经使用 effect/Brand 模块定义过品牌类型,就可以通过 Schema.fromBrand 函数复用它来定义 schema。
示例(复用已有的品牌类型)
import { Schema } from "effect"
import { Brand } from "effect"
// the existing branded type
type UserId = string & Brand.Brand<"UserId">
const UserId = Brand.nominal<UserId>()
// Define a schema for the branded type
const UserIdSchema = Schema.String.pipe(Schema.fromBrand("UserId", UserId))
使用默认构造器
Schema.brand 函数包含一个默认构造器,便于创建品牌类型的值。
import { Schema } from "effect"
const UserId = Schema.String.pipe(Schema.brand("UserId"))
const userId = UserId.make("123") // => "123"
属性签名
属性签名组合子可以分别在编码侧和解码侧独立控制 Struct 的字段。它们可以让某个键变成可选、允许 undefined、提供默认值、附加键级别的注解,或者重命名编码后的键。
基本用法
属性签名可以带注解定义,从而为字段提供额外的上下文。
示例(为属性签名添加注解)
import { Schema } from "effect"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.FiniteFromString.pipe(
Schema.annotateKey({
title: "Age", // Annotation to label the age field
}),
),
})
字段元数据请使用 Schema.annotateKey。当外部表示使用不同的键时,请在 Struct 上使用 Schema.encodeKeys。
示例(从不同的键映射)
import { Schema } from "effect"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.FiniteFromString, // Maps from "AGE" to "age"
}).pipe(Schema.encodeKeys({ age: "AGE" }))
console.log(Schema.decodeUnknownSync(Person)({ name: "name", AGE: "18" }))
// Output: { name: 'name', age: 18 }
可选字段
基本的可选属性
Schema.optional 让某个键变成可选,并在该键存在时允许 undefined。
解码
| Input | Output |
|---|---|
<missing value> | remains <missing value> |
undefined | remains undefined |
e: E | transforms to t: T |
编码
| Input | Output |
|---|---|
<missing value> | remains <missing value> |
undefined | remains undefined |
t: T | transforms back to e: E |
示例(定义可选数字字段)
import { Schema } from "effect"
const Product = Schema.Struct({
quantity: Schema.optional(Schema.FiniteFromString),
})
// ┌─── { readonly quantity?: string | undefined; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity?: number | undefined; }
// ▼
type Type = typeof Product.Type
// Decoding examples
console.log(Schema.decodeUnknownSync(Product)({ quantity: "1" }))
// Output: { quantity: 1 }
console.log(Schema.decodeUnknownSync(Product)({}))
// Output: {}
console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
// Output: { quantity: undefined }
// Encoding examples
console.log(Schema.encodeSync(Product)({ quantity: 1 }))
// Output: { quantity: "1" }
console.log(Schema.encodeSync(Product)({}))
// Output: {}
console.log(Schema.encodeSync(Product)({ quantity: undefined }))
// Output: { quantity: undefined }
可空的可选字段
当 null 应当被视为缺失值时,可以组合使用 Schema.optional、Schema.NullOr 以及可空字段变换。
解码
| Input | Output |
|---|---|
<missing value> | remains <missing value> |
undefined | remains undefined |
null | transforms to <missing value> |
e: E | transforms to t: T |
编码
| Input | Output |
|---|---|
<missing value> | remains <missing value> |
undefined | remains undefined |
t: T | transforms back to e: E |
示例(把 Null 作为缺失值处理)
import { Option, Predicate, Schema, SchemaGetter } from "effect"
const Product = Schema.Struct({
quantity: Schema.optional(Schema.NullOr(Schema.FiniteFromString)).pipe(
Schema.decodeTo(Schema.optional(Schema.Finite), {
decode: SchemaGetter.transformOptional((o) =>
o.pipe(Option.filter(Predicate.isNotNull)),
),
encode: SchemaGetter.transformOptional((o) => o),
}),
),
})
// ┌─── { readonly quantity?: string | null | undefined; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity?: number | undefined; }
// ▼
type Type = typeof Product.Type
// Decoding examples
console.log(Schema.decodeUnknownSync(Product)({ quantity: "1" }))
// Output: { quantity: 1 }
console.log(Schema.decodeUnknownSync(Product)({}))
// Output: {}
console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
// Output: { quantity: undefined }
console.log(Schema.decodeUnknownSync(Product)({ quantity: null }))
// Output: {}
// Encoding examples
console.log(Schema.encodeSync(Product)({ quantity: 1 }))
// Output: { quantity: "1" }
console.log(Schema.encodeSync(Product)({}))
// Output: {}
console.log(Schema.encodeSync(Product)({ quantity: undefined }))
// Output: { quantity: undefined }
精确的可选键
Schema.optionalKey 让某个键变成可选,但不会在该键的值类型中加入 undefined。如果该键存在,它的值必须能被所包裹的 schema 接受。
解码
| Input | Output |
|---|---|
<missing value> | remains <missing value> |
undefined | SchemaError |
e: E | transforms to t: T |
编码
| Input | Output |
|---|---|
<missing value> | remains <missing value> |
t: T | transforms back to e: E |
示例(对可选字段使用精确性)
import { Schema } from "effect"
const Product = Schema.Struct({
quantity: Schema.optionalKey(Schema.FiniteFromString),
})
// ┌─── { readonly quantity?: string; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity?: number; }
// ▼
type Type = typeof Product.Type
// Decoding examples
console.log(Schema.decodeUnknownSync(Product)({ quantity: "1" }))
// Output: { quantity: 1 }
console.log(Schema.decodeUnknownSync(Product)({}))
// Output: {}
console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
/*
throws:
SchemaError: Expected string
at ["quantity"]
*/
// Encoding examples
console.log(Schema.encodeSync(Product)({ quantity: 1 }))
// Output: { quantity: "1" }
console.log(Schema.encodeSync(Product)({}))
// Output: {}
带可空性的精确可选键
当 null 应当被视为缺失的键、而 undefined 仍应被拒绝时,可以组合使用 Schema.optionalKey、Schema.NullOr 以及可空字段变换。
解码
| Input | Output |
|---|---|
<missing value> | remains <missing value> |
null | transforms to <missing value> |
undefined | SchemaError |
e: E | transforms to t: T |
编码
| Input | Output |
|---|---|
<missing value> | remains <missing value> |
t: T | transforms back to e: E |
示例(对可选字段使用精确性并把 Null 作为缺失值处理)
import { Option, Predicate, Schema, SchemaGetter } from "effect"
const Product = Schema.Struct({
quantity: Schema.optionalKey(Schema.NullOr(Schema.FiniteFromString)).pipe(
Schema.decodeTo(Schema.optionalKey(Schema.Finite), {
decode: SchemaGetter.transformOptional((o) =>
o.pipe(Option.filter(Predicate.isNotNull)),
),
encode: SchemaGetter.transformOptional((o) => o),
}),
),
})
// ┌─── { readonly quantity?: string | null; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity?: number; }
// ▼
type Type = typeof Product.Type
// Decoding examples
console.log(Schema.decodeUnknownSync(Product)({ quantity: "1" }))
// Output: { quantity: 1 }
console.log(Schema.decodeUnknownSync(Product)({}))
// Output: {}
console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
/*
throws:
SchemaError: Expected string | null
at ["quantity"]
*/
console.log(Schema.decodeUnknownSync(Product)({ quantity: null }))
// Output: {}
// Encoding examples
console.log(Schema.encodeSync(Product)({ quantity: 1 }))
// Output: { quantity: "1" }
console.log(Schema.encodeSync(Product)({}))
// Output: {}
使用 never 类型表示可选字段
当你创建一个 schema 来复刻某个包含 never 类型可选字段的 TypeScript 类型时,例如:
type MyType = {
readonly quantity?: never
}
这些字段的处理方式取决于 tsconfig.json 中的 exactOptionalPropertyTypes 设置。
该设置会影响 schema 应当把可选的 never 类型字段视为单纯不存在,还是允许把 undefined 作为它的值。
示例(exactOptionalPropertyTypes: false)
当该特性关闭时,你可以使用 Schema.optional 函数。这种方式允许该字段隐式接受 undefined 作为值。
import { Schema } from "effect"
const Product = Schema.Struct({
quantity: Schema.optional(Schema.Never),
})
// ┌─── { readonly quantity?: undefined; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity?: undefined; }
// ▼
type Type = typeof Product.Type
示例(exactOptionalPropertyTypes: true)
当该特性开启时,请使用 Schema.optionalKey,这样该字段就只能缺失。
import { Schema } from "effect"
const Product = Schema.Struct({
quantity: Schema.optionalKey(Schema.Never),
})
// ┌─── { readonly quantity?: never; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity?: never; }
// ▼
type Type = typeof Product.Type
默认值
使用 Schema.withDecodingDefaultType 或 Schema.withDecodingDefaultTypeKey 可以提供解码后的默认值。构造器默认值是独立的,可以用 Schema.withConstructorDefault 添加。
基本默认值
这是最简单的用例。如果输入缺失或为 undefined,就会应用默认值。
| 操作 | 行为 |
|---|---|
| 解码 | 如果输入缺失或为 undefined,则应用默认值 |
| 编码 | 把输入 t: T 转换回 e: E |
示例(当字段缺失或为 undefined 时应用默认值)
import { Effect, Schema } from "effect"
const Product = Schema.Struct({
quantity: Schema.FiniteFromString.pipe(
Schema.withDecodingDefaultType(Effect.succeed(1)), // Default value for quantity
Schema.withConstructorDefault(Effect.succeed(1)),
),
})
// ┌─── { readonly quantity?: string | undefined; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity: number; }
// ▼
type Type = typeof Product.Type
// Decoding examples with default applied
console.log(Schema.decodeUnknownSync(Product)({}))
// Output: { quantity: 1 }
console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
// Output: { quantity: 1 }
console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" }))
// Output: { quantity: 2 }
// Object construction examples with default applied
console.log(Product.make({}))
// Output: { quantity: 1 }
console.log(Product.make({ quantity: 2 }))
// Output: { quantity: 2 }
键缺失时的默认值
如果默认值只应在键缺失时应用、而不应在键存在但其值为 undefined 时应用,请使用 Schema.withDecodingDefaultTypeKey。
| 操作 | 行为 |
|---|---|
| 解码 | 仅当输入缺失时应用默认值 |
| 编码 | 把输入 t: T 转换回 e: E |
示例(仅在字段缺失时应用默认值)
import { Effect, Schema } from "effect"
const Product = Schema.Struct({
quantity: Schema.FiniteFromString.pipe(
Schema.withDecodingDefaultTypeKey(Effect.succeed(1)), // Default value for quantity, only if quantity is not provided
),
})
// ┌─── { readonly quantity?: string; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity: number; }
// ▼
type Type = typeof Product.Type
console.log(Schema.decodeUnknownSync(Product)({}))
// Output: { quantity: 1 }
console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" }))
// Output: { quantity: 2 }
console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
/*
throws:
SchemaError: Expected string
at ["quantity"]
*/
带可空性的默认值
当缺失、undefined 和 null 都应产生默认值时,可以把可选且可空的字段与 SchemaGetter.transformOptional 组合起来。
| 操作 | 行为 |
|---|---|
| 解码 | 如果输入缺失,或为 undefined 或 null,则应用默认值 |
| 编码 | 把输入 t: T 转换回 e: E |
示例(当字段缺失,或为 undefined 或 null 时应用默认值)
import { Option, Predicate, Schema, SchemaGetter } from "effect"
const Product = Schema.Struct({
quantity: Schema.optional(Schema.NullOr(Schema.FiniteFromString)).pipe(
Schema.decodeTo(Schema.Finite, {
decode: SchemaGetter.transformOptional((o) =>
o.pipe(
Option.filter(Predicate.isNotNullish),
Option.orElseSome(() => 1),
),
),
encode: SchemaGetter.required(),
}),
),
})
// ┌─── { readonly quantity?: string | null | undefined; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity: number; }
// ▼
type Type = typeof Product.Type
console.log(Schema.decodeUnknownSync(Product)({}))
// Output: { quantity: 1 }
console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
// Output: { quantity: 1 }
console.log(Schema.decodeUnknownSync(Product)({ quantity: null }))
// Output: { quantity: 1 }
console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" }))
// Output: { quantity: 2 }
键缺失或为 null 时的默认值
当缺失和 null 都应产生默认值、而 undefined 应被拒绝时,请使用精确可选(exact optional)的可空字段。
| 操作 | 行为 |
|---|---|
| 解码 | 如果输入缺失或为 null,则应用默认值 |
| 编码 | 把输入 t: T 转换回 e: E |
示例(仅在字段缺失或为 null 时应用默认值)
import { Option, Predicate, Schema, SchemaGetter } from "effect"
const Product = Schema.Struct({
quantity: Schema.optionalKey(Schema.NullOr(Schema.FiniteFromString)).pipe(
Schema.decodeTo(Schema.Finite, {
decode: SchemaGetter.transformOptional((o) =>
o.pipe(
Option.filter(Predicate.isNotNull),
Option.orElseSome(() => 1),
),
),
encode: SchemaGetter.required(),
}),
),
})
// ┌─── { readonly quantity?: string | null; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity: number; }
// ▼
type Type = typeof Product.Type
console.log(Schema.decodeUnknownSync(Product)({}))
// Output: { quantity: 1 }
console.log(Schema.decodeUnknownSync(Product)({ quantity: null }))
// Output: { quantity: 1 }
console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" }))
// Output: { quantity: 2 }
console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
/*
throws:
SchemaError: Expected string | null
at ["quantity"]
*/
作为 Option 的可选字段
处理可选字段时,你可能希望把它们当作 Option 值来处理。这种方式让你能够显式地管理字段的存在或缺失,而不必依赖 undefined 或 null。
使用 Option 类型的基本可选字段
Schema.OptionFromOptional 会把缺失或为 undefined 的字段转换为 Option.none(),把已存在的值转换为 Option.some()。
解码
| Input | Output |
|---|---|
<missing value> | transforms to Option.none() |
undefined | transforms to Option.none() |
e: E | transforms to Option.some(t: T) |
编码
| Input | Output |
|---|---|
Option.none() | transforms to <missing value> |
Option.some(t: T) | transforms back to e: E |
示例(把可选字段作为 Option 处理)
import { Schema } from "effect"
const Product = Schema.Struct({
quantity: Schema.OptionFromOptional(Schema.FiniteFromString),
})
// ┌─── { readonly quantity?: string | undefined; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity: Option<number>; }
// ▼
type Type = typeof Product.Type
console.log(Schema.decodeUnknownSync(Product)({}))
// Output: { quantity: { _id: 'Option', _tag: 'None' } }
console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
// Output: { quantity: { _id: 'Option', _tag: 'None' } }
console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" }))
// Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } }
精确可选键作为 Option
Schema.OptionFromOptionalKey 会把缺失的键转换为 Option.none(),同时在键存在时拒绝 undefined。
解码
| Input | Output |
|---|---|
<missing value> | transforms to Option.none() |
undefined | SchemaError |
e: E | transforms to Option.some(t: T) |
编码
| Input | Output |
|---|---|
Option.none() | transforms to <missing value> |
Option.some(t: T) | transforms back to e: E |
示例(在可选字段作为 Option 时使用精确性)
import { Schema } from "effect"
const Product = Schema.Struct({
quantity: Schema.OptionFromOptionalKey(Schema.FiniteFromString),
})
// ┌─── { readonly quantity?: string; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity: Option<number>; }
// ▼
type Type = typeof Product.Type
console.log(Schema.decodeUnknownSync(Product)({}))
// Output: { quantity: { _id: 'Option', _tag: 'None' } }
console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" }))
// Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } }
console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
/*
throws:
SchemaError: Expected string
at ["quantity"]
*/
可空的可选字段
Schema.OptionFromOptionalNullOr 还会把 null 视为 Option.none()。
解码
| Input | Output |
|---|---|
<missing value> | transforms to Option.none() |
undefined | transforms to Option.none() |
null | transforms to Option.none() |
e: E | transforms to Option.some(t: T) |
编码
| Input | Output |
|---|---|
Option.none() | transforms to <missing value> |
Option.some(t: T) | transforms back to e: E |
示例(在可选字段作为 Option 时把 null 视为缺失值)
import { Schema } from "effect"
const Product = Schema.Struct({
quantity: Schema.OptionFromOptionalNullOr(Schema.FiniteFromString),
})
// ┌─── { readonly quantity?: string | null | undefined; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity: Option<number>; }
// ▼
type Type = typeof Product.Type
console.log(Schema.decodeUnknownSync(Product)({}))
// Output: { quantity: { _id: 'Option', _tag: 'None' } }
console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
// Output: { quantity: { _id: 'Option', _tag: 'None' } }
console.log(Schema.decodeUnknownSync(Product)({ quantity: null }))
// Output: { quantity: { _id: 'Option', _tag: 'None' } }
console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" }))
// Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } }
可空的精确可选键作为 Option
当缺失和 null 都应变成 Option.none()、而 undefined 应被拒绝时,可以把 Schema.optionalKey、Schema.NullOr 和 SchemaGetter.transformOptional 组合起来。
解码
| Input | Output |
|---|---|
<missing value> | transforms to Option.none() |
undefined | SchemaError |
null | transforms to Option.none() |
e: E | transforms to Option.some(t: T) |
编码
| Input | Output |
|---|---|
Option.none() | transforms to <missing value> |
Option.some(t: T) | transforms back to e: E |
示例(在可选字段作为 Option 时使用精确性并把 null 视为缺失值)
import { Option, Predicate, Schema, SchemaGetter } from "effect"
const Product = Schema.Struct({
quantity: Schema.optionalKey(Schema.NullOr(Schema.FiniteFromString)).pipe(
Schema.decodeTo(Schema.Option(Schema.Finite), {
decode: SchemaGetter.transformOptional((o) =>
Option.some(o.pipe(Option.filter(Predicate.isNotNull))),
),
encode: SchemaGetter.transformOptional(Option.flatten),
}),
),
})
// ┌─── { readonly quantity?: string | null; }
// ▼
type Encoded = typeof Product.Encoded
// ┌─── { readonly quantity: Option<number>; }
// ▼
type Type = typeof Product.Type
console.log(Schema.decodeUnknownSync(Product)({}))
// Output: { quantity: { _id: 'Option', _tag: 'None' } }
console.log(Schema.decodeUnknownSync(Product)({ quantity: null }))
// Output: { quantity: { _id: 'Option', _tag: 'None' } }
console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" }))
// Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } }
console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined }))
/*
throws:
SchemaError: Expected string | null
at ["quantity"]
*/
可选字段的转换
从可选到可选
使用 Schema.decodeTo 搭配 SchemaGetter.transformOptional,可以把编码侧的可选字段转换为解码侧的可选字段。这样,转换逻辑就能自行决定该键在任意一侧是否存在。
一个常见用例是把某个特定的编码值(例如空字符串)视为解码输出中缺失的字段。
解码和编码 getter 接收的是一个 Option:None 表示键缺失,返回 None 就会把它从输出中省略。
示例(从输出中省略空字符串)
考虑一个 string 类型的可选字段:输入中的空字符串应当从输出中移除。
import { Option, Schema, SchemaGetter } from "effect"
const schema = Schema.Struct({
nonEmpty: Schema.optionalKey(Schema.String).pipe(
Schema.decodeTo(Schema.optionalKey(Schema.String), {
// ┌─── Option<string>
// ▼
decode: SchemaGetter.transformOptional((maybeString) => {
if (Option.isNone(maybeString)) {
// If `maybeString` is `None`, the field is absent in the input.
// Return Option.none() to omit it in the output.
return Option.none()
}
// Extract the value from the `Some` instance
const value = maybeString.value
if (value === "") {
// Treat empty strings as missing in the output
// by returning Option.none().
return Option.none()
}
// Include non-empty strings in the output.
return Option.some(value)
}),
// In the encoding phase, you can decide to process the field
// similarly to the decoding phase or use a different logic.
// Here, the logic is left unchanged.
//
// ┌─── Option<string>
// ▼
encode: SchemaGetter.transformOptional((maybeString) => maybeString),
}),
),
})
// Decoding examples
const decode = Schema.decodeUnknownSync(schema)
console.log(decode({}))
// Output: {}
console.log(decode({ nonEmpty: "" }))
// Output: {}
console.log(decode({ nonEmpty: "a non-empty string" }))
// Output: { nonEmpty: 'a non-empty string' }
// Encoding examples
const encode = Schema.encodeSync(schema)
console.log(encode({}))
// Output: {}
console.log(encode({ nonEmpty: "" }))
// Output: { nonEmpty: '' }
console.log(encode({ nonEmpty: "a non-empty string" }))
// Output: { nonEmpty: 'a non-empty string' }
你可以用 Option.filter 简化解码逻辑,它以简洁的方式过滤掉不需要的值。
示例(使用 Option.filter 进行解码)
import { identity, Option, Schema, SchemaGetter } from "effect"
const schema = Schema.Struct({
nonEmpty: Schema.optionalKey(Schema.String).pipe(
Schema.decodeTo(Schema.optionalKey(Schema.String), {
decode: SchemaGetter.transformOptional(Option.filter((s) => s !== "")),
encode: SchemaGetter.transformOptional(identity),
}),
),
})
从可选到必需
在编码侧使用可选 schema,在解码侧使用必需 schema。当编码侧的键缺失时,SchemaGetter.transformOptional 可以提供一个值;在编码期间,它也可以省略选定的值。
示例(把 null 设为缺失字段的默认值)
这个例子在编码字段缺失时提供一个 null 值。在编码期间,解码后的 null 值会省略该字段。
import { Option, Schema, SchemaGetter } from "effect"
const schema = Schema.Struct({
nullable: Schema.optionalKey(
// Input schema for an optional string
Schema.String,
).pipe(
Schema.decodeTo(
// Output schema allowing null or string
Schema.NullOr(Schema.String),
{
// ┌─── Option<string>
// ▼
decode: SchemaGetter.transformOptional((maybeString) => {
if (Option.isNone(maybeString)) {
// If `maybeString` is `None`, the field is absent in the input.
// Return `null` as the default value for the output.
return Option.some(null)
}
// Extract the value from the `Some` instance
// and use it as the output.
return Option.some(maybeString.value)
}),
// During encoding, treat `null` as an absent field
//
// ┌─── string | null
// ▼
encode: SchemaGetter.transformOptional((maybeStringOrNull) =>
Option.flatMap(maybeStringOrNull, (stringOrNull) =>
stringOrNull === null
? // Omit the field by returning `None`
Option.none()
: // Include the field by returning `Some`
Option.some(stringOrNull),
),
),
},
),
),
})
// Decoding examples
const decode = Schema.decodeUnknownSync(schema)
console.log(decode({}))
// Output: { nullable: null }
console.log(decode({ nullable: "a value" }))
// Output: { nullable: 'a value' }
// Encoding examples
const encode = Schema.encodeSync(schema)
console.log(encode({ nullable: "a value" }))
// Output: { nullable: 'a value' }
console.log(encode({ nullable: null }))
// Output: {}
你可以用 Option.getOrElse 和 Option.liftPredicate 来精简解码与编码逻辑,写出简洁易读的转换。
示例(使用 Option.getOrElse 和 Option.liftPredicate)
import { Option, Schema, SchemaGetter } from "effect"
const schema = Schema.Struct({
nullable: Schema.optionalKey(Schema.String).pipe(
Schema.decodeTo(Schema.NullOr(Schema.String), {
decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)),
encode: SchemaGetter.transformOptional(
Option.filter((value) => value !== null),
),
}),
),
})
从必需到可选
在编码侧使用必需 schema,在解码侧使用可选 schema。这种转换可以省略选定的解码值,并且必须在编码期间恢复出一个必需的值。
示例(把空字符串视为缺失值)
在这个例子中,name 字段是必需的,但如果它的值是空字符串,就会被当作可选。解码时,name 中的空字符串被视为缺失;编码时则保证一定有一个值(如果 name 缺失,就用空字符串作为默认值)。
import { Option, Schema, SchemaGetter } from "effect"
const schema = Schema.Struct({
name: Schema.String.pipe(
Schema.decodeTo(Schema.optionalKey(Schema.String), {
// ┌─── Option<string>
// ▼
decode: SchemaGetter.transformOptional((maybeString) =>
Option.flatMap(maybeString, (string) => {
// Treat empty string as a missing value
if (string === "") {
// Omit the field by returning `None`
return Option.none()
}
// Otherwise, return the string as is
return Option.some(string)
}),
),
// ┌─── Option<string>
// ▼
encode: SchemaGetter.transformOptional((maybeString) => {
// Check if the field is missing
if (Option.isNone(maybeString)) {
// Provide an empty string as default
return Option.some("")
}
// Otherwise, return the string as is
return maybeString
}),
}),
),
})
// Decoding examples
const decode = Schema.decodeUnknownSync(schema)
console.log(decode({ name: "John" }))
// Output: { name: 'John' }
console.log(decode({ name: "" }))
// Output: {}
// Encoding examples
const encode = Schema.encodeSync(schema)
console.log(encode({ name: "John" }))
// Output: { name: 'John' }
console.log(encode({}))
// Output: { name: '' }
你可以用 Option.liftPredicate 和 Option.getOrElse 来精简解码与编码逻辑,写出简洁易读的转换。
示例(使用 Option.liftPredicate 和 Option.getOrElse)
import { Option, Schema, SchemaGetter } from "effect"
const schema = Schema.Struct({
name: Schema.String.pipe(
Schema.decodeTo(Schema.optionalKey(Schema.String), {
decode: SchemaGetter.transformOptional((maybeString) =>
Option.flatMap(
maybeString,
Option.liftPredicate((s) => s !== ""),
),
),
encode: SchemaGetter.transformOptional((maybeString) =>
Option.some(Option.getOrElse(maybeString, () => "")),
),
}),
),
})
扩展 schema
Struct schema 会暴露自己的 fields,你可以把它展开到新的 struct 中,也可以用 Schema.fieldsAssign 来扩展。Union 会暴露 mapMembers,因此可以把同一个字段操作应用到每个 struct 成员上。
对 ...Struct.fields 使用字段展开时,schema 会保持 Struct 类型,
这样你就可以继续访问 fields 属性来做进一步的修改。
展开 Struct 的字段
Struct 通过 fields 属性提供对其字段的访问,这让你可以扩展已有的 struct:既能添加额外的字段,也能把多个 struct 的字段合并起来。
示例(添加新字段)
import { Schema } from "effect"
const Original = Schema.Struct({
a: Schema.String,
b: Schema.String,
})
const Extended = Schema.Struct({
...Original.fields,
// Adding new fields
c: Schema.String,
d: Schema.String,
})
// ┌─── {
// | readonly a: string;
// | readonly b: string;
// | readonly c: string;
// | readonly d: string;
// | }
// ▼
type Type = typeof Extended.Type
示例(添加额外的索引签名)
import { Schema } from "effect"
const Original = Schema.Struct({
a: Schema.String,
b: Schema.String,
})
const Extended = Schema.StructWithRest(
Schema.Struct(Original.fields),
// Adding an index signature
[Schema.Record(Schema.String, Schema.String)],
)
// ┌─── {
// │ readonly [x: string]: string;
// | readonly a: string;
// | readonly b: string;
// | }
// ▼
type Type = typeof Extended.Type
示例(合并多个 struct 的字段)
import { Schema } from "effect"
const Struct1 = Schema.Struct({
a: Schema.String,
b: Schema.String,
})
const Struct2 = Schema.Struct({
c: Schema.String,
d: Schema.String,
})
const Extended = Schema.Struct({
...Struct1.fields,
...Struct2.fields,
})
// ┌─── {
// | readonly a: string;
// | readonly b: string;
// | readonly c: string;
// | readonly d: string;
// | }
// ▼
type Type = typeof Extended.Type
fieldsAssign 函数
Schema.fieldsAssign(fields) 是 struct.mapFields(Struct.assign(fields)) 的简洁写法。你可以直接在 struct 上使用它,也可以把它映射到 union 的每个成员上。
示例(为每个 union 成员添加字段)
import { Schema, Tuple } from "effect"
const Struct = Schema.Struct({
a: Schema.String,
})
const UnionOfStructs = Schema.Union([
Schema.Struct({ b: Schema.String }),
Schema.Struct({ c: Schema.String }),
])
const Extended = UnionOfStructs.mapMembers(
Tuple.map(Schema.fieldsAssign(Struct.fields)),
)
// ┌─── {
// | readonly a: string;
// | } & ({
// | readonly b: string;
// | } | {
// | readonly c: string;
// | })
// ▼
type Type = typeof Extended.Type
重命名属性
在定义时重命名属性
如果希望在编码表示中使用不同的键,请在定义 struct 之后应用 Schema.encodeKeys。
示例(重命名必需属性)
import { Schema } from "effect"
const schema = Schema.Struct({
a: Schema.String,
b: Schema.Finite,
}).pipe(Schema.encodeKeys({ a: "c" }))
// ┌─── { readonly c: string; readonly b: number; }
// ▼
type Encoded = typeof schema.Encoded
// ┌─── { readonly a: string; readonly b: number; }
// ▼
type Type = typeof schema.Type
console.log(Schema.decodeUnknownSync(schema)({ c: "c", b: 1 }))
// Output: { a: "c", b: 1 }
示例(重命名可选属性)
import { Schema } from "effect"
const schema = Schema.Struct({
a: Schema.optional(Schema.String),
b: Schema.Finite,
}).pipe(Schema.encodeKeys({ a: "c" }))
// ┌─── { readonly b: number; readonly c?: string | undefined; }
// ▼
type Encoded = typeof schema.Encoded
// ┌─── { readonly a?: string | undefined; readonly b: number; }
// ▼
type Type = typeof schema.Type
console.log(Schema.decodeUnknownSync(schema)({ c: "c", b: 1 }))
// Output: { a: 'c', b: 1 }
console.log(Schema.decodeUnknownSync(schema)({ b: 1 }))
// Output: { b: 1 }
重命名已有 schema 的属性
对于已有的 struct,用 mapFields 重命名其解码后的字段,然后用 Schema.encodeKeys 在编码表示中保留原来的名字。对于 union,把同样的操作应用到每个成员上。
示例(重命名 struct schema 中的属性)
import { Schema, Struct } from "effect"
const Original = Schema.Struct({
c: Schema.String,
b: Schema.Finite,
})
// Renaming the "c" property to "a"
//
//
// ┌─── Struct<{
// | readonly a: string;
// | readonly b: number;
// | }>
// ▼
const Renamed = Original.mapFields((fields) => ({
a: fields.c,
...Struct.omit(fields, ["c"]),
})).pipe(Schema.encodeKeys({ a: "c" }))
console.log(Schema.decodeUnknownSync(Renamed)({ c: "c", b: 1 }))
// Output: { a: "c", b: 1 }
示例(重命名 union schema 中的属性)
import { Schema } from "effect"
const Original = Schema.Union([
Schema.Struct({
a: Schema.String,
b: Schema.Finite,
}),
Schema.Struct({
a: Schema.String,
d: Schema.Boolean,
}),
])
// Use "c" for "a" in the encoded representation of every member
const Renamed = Original.mapMembers(
([first, second]) =>
[
first.pipe(Schema.encodeKeys({ a: "c" })),
second.pipe(Schema.encodeKeys({ a: "c" })),
] as const,
)
console.log(Schema.decodeUnknownSync(Renamed)({ c: "c", b: 1 }))
// Output: { a: "c", b: 1 }
console.log(Schema.decodeUnknownSync(Renamed)({ c: "c", d: false }))
// Output: { a: "c", d: false }
递归 schema
Schema.suspend 函数用于定义引用自身的 schema,例如递归数据结构中的 schema。
示例(自引用 schema)
在这个例子中,Category schema 通过 subcategories 字段引用自身,该字段是一个由 Category 对象组成的数组。
import { Schema } from "effect"
interface Category {
readonly name: string
readonly subcategories: ReadonlyArray<Category>
}
const Category = Schema.Struct({
name: Schema.String,
subcategories: Schema.Array(
Schema.suspend((): Schema.Codec<Category> => Category),
),
})
必须定义 Category 类型并添加显式的类型注解,否则 TypeScript 将难以正确推断
类型。没有这个注解,你可能会遇到如下错误信息:
示例(类型推断错误)
import { Schema } from "effect"
// @errors: 7022
const Category = Schema.Struct({
name: Schema.String,
// @errors: 7022 7024
subcategories: Schema.Array(Schema.suspend(() => Category)),
})
简化 schema 定义的实用模式
正如我们所见,为了能够定义递归 schema,必须为 schema 的 Type 定义一个 interface,
这会让事情变得复杂,而且相当繁琐。
缓解这一问题的一种模式是,把负责递归的字段与所有其他字段分离开来。
示例(分离递归字段)
import { Schema } from "effect"
const fields = {
name: Schema.String,
// ...other fields as needed
}
// Define an interface for the Category schema,
// extending the Type of the defined fields
interface Category extends Schema.Struct.Type<typeof fields> {
// Define `subcategories` using recursion
readonly subcategories: ReadonlyArray<Category>
}
const Category = Schema.Struct({
...fields, // Spread in the base fields
subcategories: Schema.Array(
// Define `subcategories` using recursion
Schema.suspend((): Schema.Codec<Category> => Category),
),
})
相互递归的 schema
你也可以使用 Schema.suspend 创建相互递归的 schema,即两个 schema 互相引用。在下面的例子中,Expression 和 Operation 通过相互引用构成一棵简单的算术表达式树。
示例(定义相互递归的 schema)
import { Schema } from "effect"
interface Expression {
readonly type: "expression"
readonly value: number | Operation
}
interface Operation {
readonly type: "operation"
readonly operator: "+" | "-"
readonly left: Expression
readonly right: Expression
}
const Expression = Schema.Struct({
type: Schema.Literal("expression"),
value: Schema.Union([
Schema.Finite,
Schema.suspend((): Schema.Codec<Operation> => Operation),
]),
})
const Operation = Schema.Struct({
type: Schema.Literal("operation"),
operator: Schema.Literals(["+", "-"]),
left: Expression,
right: Expression,
})
Encoded 与 Type 不同的递归类型
定义 Encoded 类型与 Type 类型不同的递归 schema 会再增加一层复杂度。在这种情况下,我们需要定义两个 interface:一个用于 Type 类型(如前所见),另一个用于 Encoded 类型。
示例(Encoded 与 Type 定义不同的递归 schema)
让我们看一个 id 字段由 Schema.FiniteFromString 定义的例子。
它的 Type 是 number,而它的 Encoded 类型是 string。
当我们把这个字段添加到 Category schema 时,TypeScript 会报错:
import { Schema } from "effect"
const fields = {
id: Schema.FiniteFromString,
name: Schema.String,
}
interface Category extends Schema.Struct.Type<typeof fields> {
readonly subcategories: ReadonlyArray<Category>
}
const Category = Schema.Struct({
...fields,
subcategories: Schema.Array(
// @errors: 2322
Schema.suspend((): Schema.Codec<Category> => Category),
),
})
这样写会失败,因为 Schema.Codec<Category> 会把编码类型默认为 Category。递归的边还必须指定 CategoryEncoded:
import { Schema } from "effect"
const fields = {
id: Schema.FiniteFromString,
name: Schema.String,
}
interface Category extends Schema.Struct.Type<typeof fields> {
readonly subcategories: ReadonlyArray<Category>
}
interface CategoryEncoded extends Schema.Struct.Encoded<typeof fields> {
readonly subcategories: ReadonlyArray<CategoryEncoded>
}
const Category = Schema.Struct({
...fields,
subcategories: Schema.Array(
Schema.suspend((): Schema.Codec<Category, CategoryEncoded> => Category),
),
})