基本用法
学习定义和使用基础 schema,包括原始类型、字面量、联合和 Struct,以实现有效的数据校验与转换。
原始类型
Schema 模块为常见的原始类型提供了内置 schema。
| Schema | 等价的 TypeScript 类型 |
|---|---|
Schema.String | string |
Schema.Finite | number |
Schema.Boolean | boolean |
Schema.BigInt | bigint |
Schema.Symbol | symbol |
Schema.ObjectKeyword | object |
Schema.Undefined | undefined |
Schema.Void | void |
Schema.Any | any |
Schema.Unknown | unknown |
Schema.Never | never |
示例(使用原始类型 schema)
import { Schema } from "effect"
const schema = Schema.String
// Infers the type as string
//
// ┌─── string
// ▼
type Type = typeof schema.Type
// Attempt to decode a null value, which will throw a SchemaError
Schema.decodeUnknownSync(schema)(null)
/*
throws:
SchemaError: Expected string
*/
revealCodec
为了更方便地使用 schema,内置 schema 在可能的情况下会以更简短的不透明类型暴露。
Schema.revealCodec 函数会返回同一个 schema 值,但将其拓宽为完整的 Codec<T, E, RD, RE> 视图,促使 TypeScript 推断出全部四个参数,且没有任何运行时开销。
示例(展开完整的 Codec 视图)
例如,Schema.String 的具体类型是 typeof Schema.String。把它传给 Schema.revealCodec 会暴露出它的完整视图 Codec<string, string, never, never>。
import { Schema } from "effect"
// ┌─── typeof Schema.String
// ▼
const schema = Schema.String
// ┌─── Codec<string, string, never, never>
// ▼
const codec = Schema.revealCodec(schema)
唯一 Symbol
你可以使用 Schema.UniqueSymbol 为唯一 symbol 创建 schema。
示例(为唯一 symbol 创建 schema)
import { Schema } from "effect"
const mySymbol = Symbol.for("mySymbol")
const schema = Schema.UniqueSymbol(mySymbol)
// ┌─── typeof mySymbol
// ▼
type Type = typeof schema.Type
Schema.decodeUnknownSync(schema)(null)
/*
throws:
SchemaError: Expected Symbol(mySymbol)
*/
字面量
字面量 schema 表示一种字面量类型。你可以用它们来指定某个类型必须具有的确切值。
字面量可以是以下类型:
stringnumberbooleannullbigint
示例(定义字面量 schema)
import { Schema } from "effect"
// Define various literal schemas
Schema.Null // Same as Schema.Literal(null)
Schema.Literal("a") // string literal
Schema.Literal(1) // number literal
Schema.Literal(true) // boolean literal
Schema.Literal(2n) // BigInt literal
示例(为 "a" 定义字面量 schema)
import { Schema } from "effect"
// ┌─── Literal<"a">
// ▼
const schema = Schema.Literal("a")
// ┌─── "a"
// ▼
type Type = typeof schema.Type
console.log(Schema.decodeUnknownSync(schema)("a"))
// Output: "a"
console.log(Schema.decodeUnknownSync(schema)("b"))
/*
throws:
SchemaError: Expected "a"
*/
字面量联合
你可以把多个字面量作为参数传给 Schema.Literals 构造器,从而创建它们的联合:
示例(定义字面量联合)
import { Schema } from "effect"
// ┌─── Literals<["a", "b", "c"]>
// ▼
const schema = Schema.Literals(["a", "b", "c"])
// ┌─── "a" | "b" | "c"
// ▼
type Type = typeof schema.Type
Schema.decodeUnknownSync(schema)(null)
/*
throws:
SchemaError: Expected "a" | "b" | "c"
*/
你可以为整个联合添加注解,以替换其默认错误信息(见自定义错误信息)。
示例(为字面量联合添加自定义信息)
import { Schema } from "effect"
// Schema with individual messages for each literal
const individualMessages = Schema.Literals(["a", "b", "c"])
console.log(Schema.decodeUnknownSync(individualMessages)(null))
/*
throws:
SchemaError: Expected "a" | "b" | "c"
*/
// Schema with a unified custom message for all literals
const unifiedMessage = Schema.Literals(["a", "b", "c"]).annotate({
message: "Not a valid code",
})
console.log(Schema.decodeUnknownSync(unifiedMessage)(null))
/*
throws:
SchemaError: Not a valid code
*/
暴露的值
你可以通过 literals 属性访问字面量 schema 中定义的字面量:
import { Schema } from "effect"
const schema = Schema.Literals(["a", "b", "c"])
// ┌─── readonly ["a", "b", "c"]
// ▼
const literals = schema.literals // => ["a", "b", "c"]
挑选字面量
你可以使用 Schema.Literals 值的 .pick 方法缩小其可能的取值范围。
示例(挑选字面量的子集)
import { Schema } from "effect"
// Create a schema for a subset of literals ("a" and "b") from a larger set
//
// ┌─── Literals<["a", "b"]>
// ▼
const schema = Schema.Literals(["a", "b", "c"]).pick(["a", "b"])
有时,你可能需要在代码的其他部分复用一个字面量 schema。下面是一个演示如何做到这一点的示例:
示例(从字面量 schema 创建子类型)
import { Schema } from "effect"
// Define the base set of fruit categories
const FruitCategory = Schema.Literals(["sweet", "citrus", "tropical"])
// Define a general Fruit schema with the base category set
const Fruit = Schema.Struct({
id: Schema.Finite,
category: FruitCategory,
})
// Define a specific Fruit schema for only "sweet" and "citrus" categories
const SweetAndCitrusFruit = Schema.Struct({
id: Schema.Finite,
category: FruitCategory.pick(["sweet", "citrus"]),
})
在这个示例中,FruitCategory 是各种水果类别的唯一事实来源。我们复用它创建了 Fruit 的一个子类型 SweetAndCitrusFruit,确保只允许指定的类别("sweet" 和 "citrus")。这种做法有助于在整个代码中保持一致,并在类别定义发生变化时提供类型安全。
模板字面量
在 TypeScript 中,模板字面量类型允许你在字符串字面量中嵌入表达式。Schema.TemplateLiteral 构造器让你可以为这些模板字面量类型创建 schema。
示例(定义模板字面量)
import { Schema } from "effect"
// This creates a schema for: `a${string}`
//
// ┌─── TemplateLiteral<readonly ["a", typeof Schema.String]>
// ▼
const schema1 = Schema.TemplateLiteral(["a", Schema.String])
// This creates a schema for:
// `https://${string}.com` | `https://${string}.net`
const schema2 = Schema.TemplateLiteral([
"https://",
Schema.String,
".",
Schema.Literals(["com", "net"]),
])
示例(来自模板字面量类型文档)
来看一个更复杂的例子。假设你有两套用于邮件和页脚的 locale ID。你可以使用 Schema.TemplateLiteral 构造器创建一个组合这些 ID 的 schema:
import { Schema } from "effect"
const EmailLocaleIDs = Schema.Literals(["welcome_email", "email_heading"])
const FooterLocaleIDs = Schema.Literals(["footer_title", "footer_sendoff"])
// This creates a schema for:
// "welcome_email_id" | "email_heading_id" |
// "footer_title_id" | "footer_sendoff_id"
const schema = Schema.TemplateLiteral([
Schema.Union([EmailLocaleIDs, FooterLocaleIDs]),
"_id",
])
支持的片段类型
Schema.TemplateLiteral 构造器支持以下类型的片段(span):
Schema.StringSchema.Finite- 字面量:
string | number | boolean | null | bigint。它们既可以用Schema.Literal包装,也可以直接使用 - 上述类型的联合
- 上述类型的品牌类型(Brand)
示例(在模板字面量中使用品牌类型字符串)
import { Schema } from "effect"
// Create a branded string schema for an authorization token
const AuthorizationToken = Schema.String.pipe(
Schema.brand("AuthorizationToken"),
)
// This creates a schema for:
// `Bearer ${string & Brand<"AuthorizationToken">}`
const schema = Schema.TemplateLiteral(["Bearer ", AuthorizationToken])
TemplateLiteralParser
Schema.TemplateLiteral 构造器作为简单的校验器很有用,但它只会把模板字面量定义转换成正则表达式,以此验证输入是否符合某个特定的字符串模式。类似地,Schema.isPattern 也直接使用正则表达式来达到同样的目的。校验之后,这两种方式都需要额外的手动解析,才能把校验过的字符串转换成可用的数据格式。
为了解决这些局限并免去校验后的手动解析,我们开发了 Schema.TemplateLiteralParser API。它不仅校验输入格式,还会自动把它解析为结构更清晰、类型更安全的输出,具体来说是一个元组(tuple)格式。
Schema.TemplateLiteralParser 构造器支持与 Schema.TemplateLiteral 相同类型的片段。
示例(使用 TemplateLiteralParser 进行解析与编码)
import { Schema } from "effect"
const schema = Schema.TemplateLiteralParser([
Schema.FiniteFromString,
"a",
Schema.NonEmptyString,
])
console.log(Schema.decodeSync(schema)("100afoo"))
// Output: [ 100, 'a', 'foo' ]
console.log(Schema.encodeSync(schema)([100, "a", "foo"]))
// Output: '100afoo'
原生枚举
Schema 模块支持 TypeScript 的原生枚举。你可以使用 Schema.Enum 为枚举定义 schema,从而校验属于该枚举的值。
示例(为枚举定义 schema)
import { Schema } from "effect"
enum Fruits {
Apple,
Banana,
}
// ┌─── Enum<typeof Fruits>
// ▼
const schema = Schema.Enum(Fruits)
//
// ┌─── Fruits
// ▼
type Type = typeof schema.Type
暴露的值
枚举可以通过 schema 的 enums 属性访问。你可以用这个属性获取单个成员或整个枚举值集合。
import { Schema } from "effect"
enum Fruits {
Apple,
Banana,
}
const schema = Schema.Enum(Fruits)
schema.enums // Returns all enum members
schema.enums.Apple // Access the Apple member
schema.enums.Banana // Access the Banana member
联合类型
Schema 模块内置了 Schema.Union 构造器,用于创建「OR」类型,让你可以定义能表示多种类型的 schema。
示例(定义联合 schema)
import { Schema } from "effect"
// ┌─── Union<[typeof Schema.String, typeof Schema.Finite]>
// ▼
const schema = Schema.Union([Schema.String, Schema.Finite])
// ┌─── string | number
// ▼
type Type = typeof schema.Type
联合成员的求值顺序
解码时,联合成员会按照定义顺序依次求值。如果某个值与第一个成员匹配,就会使用该 schema 对它解码;如果不匹配,解码过程会继续尝试下一个成员。
如果多个 schema 都能解码同一个值,顺序就很关键。把更通用的 schema 放在更具体的 schema 之前,可能会导致属性丢失,因为会使用第一个匹配的 schema。
示例(处理联合中相互重叠的 schema)
import { Schema } from "effect"
// Define two overlapping schemas
const Member1 = Schema.Struct({
a: Schema.String,
})
const Member2 = Schema.Struct({
a: Schema.String,
b: Schema.Finite,
})
// ❌ Define a union where Member1 appears first
const Bad = Schema.Union([Member1, Member2])
console.log(Schema.decodeUnknownSync(Bad)({ a: "a", b: 12 }))
// Output: { a: 'a' } (Member1 matched first, so `b` was ignored)
// ✅ Define a union where Member2 appears first
const Good = Schema.Union([Member2, Member1])
console.log(Schema.decodeUnknownSync(Good)({ a: "a", b: 12 }))
// Output: { a: 'a', b: 12 } (Member2 matched first, so `b` was included)
字面量联合
你固然可以通过组合各个字面量 schema 来创建字面量联合:
示例(使用各个字面量 schema)
import { Schema } from "effect"
// ┌─── Union<[Schema.Literal<"a">, Schema.Literal<"b">, Schema.Literal<"c">]>
// ▼
const schema = Schema.Union([
Schema.Literal("a"),
Schema.Literal("b"),
Schema.Literal("c"),
])
你可以把多个字面量直接传给 Schema.Literals 构造器,从而简化这一过程:
示例(定义字面量联合)
import { Schema } from "effect"
// ┌─── Literals<["a", "b", "c"]>
// ▼
const schema = Schema.Literals(["a", "b", "c"])
// ┌─── "a" | "b" | "c"
// ▼
type Type = typeof schema.Type
你可以为整个联合添加注解,以替换其默认错误信息(见自定义错误信息)。
示例(为字面量联合添加自定义信息)
import { Schema } from "effect"
// Schema with individual messages for each literal
const individualMessages = Schema.Literals(["a", "b", "c"])
console.log(Schema.decodeUnknownSync(individualMessages)(null))
/*
throws:
SchemaError: Expected "a" | "b" | "c"
*/
// Schema with a unified custom message for all literals
const unifiedMessage = Schema.Literals(["a", "b", "c"]).annotate({
message: "Not a valid code",
})
console.log(Schema.decodeUnknownSync(unifiedMessage)(null))
/*
throws:
SchemaError: Not a valid code
*/
可空类型
Schema 模块提供了一些工具函数,用于定义允许可空类型的 schema,帮助你处理可能是 null、undefined 或两者兼有的值。
示例(创建可空 Schema)
import { Schema } from "effect"
// Represents a schema for a string or null value
Schema.NullOr(Schema.String)
// Represents a schema for a string, null, or undefined value
Schema.NullishOr(Schema.String)
// Represents a schema for a string or undefined value
Schema.UndefinedOr(Schema.String)
可辨识联合
TypeScript 中的可辨识联合是一种对复杂数据结构建模的方式,这类结构可能根据一组特定的条件或属性呈现不同的形态。它允许你定义一个表示多个相关形状的类型,其中每个形状都由一个共享的判别属性唯一标识。
在可辨识联合中,联合的每个变体都有一个公共属性,称为判别属性(discriminant)。判别属性是字面量类型,这意味着它只能取有限的一组可能值。TypeScript 可以根据判别属性的值推断出当前使用的是联合中的哪个变体。
示例(在 TypeScript 中定义可辨识联合)
type Circle = {
readonly kind: "circle"
readonly radius: number
}
type Square = {
readonly kind: "square"
readonly sideLength: number
}
type Shape = Circle | Square
在 Schema 模块中,你可以为每个类型指定一个字面量字段作为判别属性,从而以类似的方式定义可辨识联合。
示例(使用 Schema 定义可辨识联合)
import { Schema } from "effect"
const Circle = Schema.Struct({
kind: Schema.Literal("circle"),
radius: Schema.Finite,
})
const Square = Schema.Struct({
kind: Schema.Literal("square"),
sideLength: Schema.Finite,
})
const Shape = Schema.Union([Circle, Square])
在这个例子中,Schema.Literal 构造器把 kind 属性设置为 Circle 和 Square 两个 schema 共同的判别属性。随后 Shape schema 表示这两个类型的联合,让 TypeScript 能够根据 kind 的值推断出具体的形状。
把简单联合转换为可辨识联合
如果你从一个简单联合开始,并想把它转换为可辨识联合,可以为每个成员添加一个特殊属性。这样 TypeScript 就能根据判别属性的值自动推断出正确的类型。
示例(最初的简单联合)
例如,假设你定义了一个由 Circle 和 Square 组合而成、不带任何特殊属性的 Shape 联合:
import { Schema } from "effect"
const Circle = Schema.Struct({
radius: Schema.Finite,
})
const Square = Schema.Struct({
sideLength: Schema.Finite,
})
const Shape = Schema.Union([Circle, Square])
为了让代码更易于管理,你可能想把简单联合转换为可辨识联合。这样,TypeScript 就能根据某个特定属性的值自动判断你正在处理联合中的哪个成员。
为此,你可以为联合的每个成员添加一个特殊属性,让 TypeScript 在运行时知道它面对的是哪个类型。
下面演示如何把 Shape schema 转换为另一个表示可辨识联合的 schema:
示例(添加判别属性)
import { Schema, SchemaTransformation } from "effect"
const Circle = Schema.Struct({
radius: Schema.Finite,
})
const Square = Schema.Struct({
sideLength: Schema.Finite,
})
const DiscriminatedShape = Schema.Union([
Circle.pipe(
Schema.decodeTo(
// Add a "kind" property with the literal value "circle" to Circle
Schema.Struct({ ...Circle.fields, kind: Schema.Literal("circle") }),
SchemaTransformation.transform({
// Add the discriminant property to Circle
decode: (circle) => ({ ...circle, kind: "circle" as const }),
// Remove the discriminant property
encode: ({ kind: _kind, ...rest }) => rest,
}),
),
),
Square.pipe(
Schema.decodeTo(
// Add a "kind" property with the literal value "square" to Square
Schema.Struct({ ...Square.fields, kind: Schema.Literal("square") }),
SchemaTransformation.transform({
// Add the discriminant property to Square
decode: (square) => ({ ...square, kind: "square" as const }),
// Remove the discriminant property
encode: ({ kind: _kind, ...rest }) => rest,
}),
),
),
])
console.log(Schema.decodeUnknownSync(DiscriminatedShape)({ radius: 10 }))
// Output: { radius: 10, kind: 'circle' }
console.log(Schema.decodeUnknownSync(DiscriminatedShape)({ sideLength: 10 }))
// Output: { sideLength: 10, kind: 'square' }
前面这个方案可行,但需要大量样板代码。你可以用 mapFields 添加判别属性,并使用 Schema.tagDefaultOmit 提供解码时的默认值,同时在编码时省略它:
示例(使用 Schema.tagDefaultOmit)
import { Schema } from "effect"
const Circle = Schema.Struct({
radius: Schema.Finite,
})
const Square = Schema.Struct({
sideLength: Schema.Finite,
})
const DiscriminatedShape = Schema.Union([
Circle.mapFields((fields) => ({
...fields,
kind: Schema.tagDefaultOmit("circle"),
})),
Square.mapFields((fields) => ({
...fields,
kind: Schema.tagDefaultOmit("square"),
})),
])
// decoding
console.log(Schema.decodeUnknownSync(DiscriminatedShape)({ radius: 10 }))
// Output: { radius: 10, kind: 'circle' }
// encoding
console.log(
Schema.encodeSync(DiscriminatedShape)({
kind: "circle",
radius: 10,
}),
)
// Output: { radius: 10 }
暴露的值
你可以访问以元组形式表示的联合 schema 中的各个成员:
import { Schema } from "effect"
const schema = Schema.Union([Schema.String, Schema.Finite])
// Accesses the members of the union
const members = schema.members
// ┌─── typeof Schema.String
// ▼
const firstMember = members[0]
// ┌─── typeof Schema.Finite
// ▼
const secondMember = members[1]
元组
Schema 模块允许你定义元组,即元素类型可以不同的有序集合。 你可以定义包含必需元素、可选元素或剩余元素的元组。
必需元素
要定义包含必需元素的元组,可以使用 Schema.Tuple 构造器,按顺序列出各个元素 schema 即可:
示例(定义包含必需元素的元组)
import { Schema } from "effect"
// Define a tuple with a string and a number as required elements
//
// ┌─── Tuple<[typeof Schema.String, typeof Schema.Finite]>
// ▼
const schema = Schema.Tuple([Schema.String, Schema.Finite])
// ┌─── readonly [string, number]
// ▼
type Type = typeof schema.Type
追加必需元素
你可以使用展开运算符,向已有元组追加额外的必需元素:
示例(向已有元组添加元素)
import { Schema } from "effect"
const tuple1 = Schema.Tuple([Schema.String, Schema.Finite])
// Append a boolean to the existing tuple
const tuple2 = Schema.Tuple([...tuple1.elements, Schema.Boolean])
// ┌─── readonly [string, number, boolean]
// ▼
type Type = typeof tuple2.Type
可选元素
要定义可选元素,请使用 Schema.optionalKey 构造器。
示例(定义包含可选元素的元组)
import { Schema } from "effect"
// Define a tuple with a required string and an optional number
const schema = Schema.Tuple([
Schema.String, // required element
Schema.optionalKey(Schema.Finite), // optional element
])
// ┌─── readonly [string, number?]
// ▼
type Type = typeof schema.Type
剩余元素
要定义剩余元素,请把它添加在必需元素或可选元素列表之后。 剩余元素让元组可以接受特定类型的额外元素。
示例(使用剩余元素)
import { Schema } from "effect"
// Define a tuple with required elements and a rest element of type boolean
const schema = Schema.TupleWithRest(
Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Finite)]), // elements
[Schema.Boolean], // rest element
)
type Type = typeof schema.Type
你还可以在剩余元素之后包含其他元素:
示例(在剩余元素之后包含额外元素)
import { Schema } from "effect"
// Define a tuple with required elements, a rest element,
// and an additional element
const schema = Schema.TupleWithRest(
Schema.Tuple([Schema.String, Schema.UndefinedOr(Schema.Finite)]), // elements
[Schema.Boolean, Schema.String], // rest element, then an additional element
)
type Type = typeof schema.Type
元素注解
注解(annotation)可用于为元组元素添加元数据,从而更容易描述它们的用途或要求。 这在生成文档或 JSON schema 时尤其有用。
示例(为元组元素添加注解)
import { Schema } from "effect"
// Define a tuple representing a point with annotations for each coordinate
const Point = Schema.Tuple([
Schema.Finite.annotateKey({
title: "X",
description: "X coordinate",
}),
Schema.optionalKey(Schema.Finite).annotateKey({
title: "Y",
description: "optional Y coordinate",
}),
])
// Generate a JSON Schema from the tuple
console.log(Schema.toJsonSchemaDocument(Point))
/*
Output:
{
dialect: 'draft-2020-12',
schema: {
type: 'array',
prefixItems: [
{
type: 'number',
allOf: [{ title: 'X', description: 'X coordinate' }]
},
{
type: 'number',
allOf: [{ title: 'Y', description: 'optional Y coordinate' }]
}
],
maxItems: 2,
minItems: 1
},
definitions: {}
}
*/
暴露的值
你可以使用 elements 和 rest 属性访问元组 schema 的元素与剩余元素:
示例(访问元组 schema 的元素与剩余元素)
import { Schema } from "effect"
// Define a tuple with required, optional, and rest elements
const schema = Schema.TupleWithRest(
Schema.Tuple([Schema.String, Schema.UndefinedOr(Schema.Finite)]), // elements
[Schema.Boolean, Schema.String], // rest element, then an additional element
)
// Access the required and optional elements of the tuple
//
// ┌─── readonly [typeof Schema.String, Schema.UndefinedOr<typeof Schema.Finite>]
// ▼
const tupleElements = schema.schema.elements
// Access the rest element of the tuple
//
// ┌─── readonly [typeof Schema.Boolean, typeof Schema.String]
// ▼
const restElement = schema.rest
数组
Schema 模块允许你为数组定义 schema,从而轻松校验由特定类型的元素组成的集合。
示例(定义数组 Schema)
import { Schema } from "effect"
// Define a schema for an array of numbers
//
// ┌─── $Array<typeof Schema.Finite>
// ▼
const schema = Schema.Array(Schema.Finite)
// ┌─── readonly number[]
// ▼
type Type = typeof schema.Type
可变数组
默认情况下,Schema.Array 生成的类型被标记为 readonly。
要为可变数组创建 schema,可以使用 Schema.mutable 函数,它以浅层方式让数组类型变为可变。
示例(创建可变数组 Schema)
import { Schema } from "effect"
// Define a schema for a mutable array of numbers
//
// ┌─── mutable<Schema.$Array<typeof Schema.Finite>>
// ▼
const schema = Schema.mutable(Schema.Array(Schema.Finite))
// ┌─── number[]
// ▼
type Type = typeof schema.Type
暴露的值
你可以使用 value 属性访问数组 schema 的值类型:
示例(访问数组 Schema 的值类型)
import { Schema } from "effect"
const schema = Schema.Array(Schema.Finite)
// Access the value type of the array schema
//
// ┌─── typeof Schema.Finite
// ▼
const value = schema.value
非空数组
Schema 模块还提供了为非空数组定义 schema 的方式,确保数组始终至少包含一个元素。
示例(定义非空数组 Schema)
import { Schema } from "effect"
// Define a schema for a non-empty array of numbers
//
// ┌─── NonEmptyArray<typeof Schema.Finite>
// ▼
const schema = Schema.NonEmptyArray(Schema.Finite)
// ┌─── readonly [number, ...number[]]
// ▼
type Type = typeof schema.Type
暴露的值
你可以使用 value 属性访问非空数组 schema 的值类型:
示例(访问非空数组 schema 的值类型)
import { Schema } from "effect"
// Define a schema for a non-empty array of numbers
const schema = Schema.NonEmptyArray(Schema.Finite)
// Access the value type of the non-empty array schema
//
// ┌─── typeof Schema.Finite
// ▼
const value = schema.value
Record
Schema 模块提供了定义 record 类型的支持:record 是键值对的集合,其中的键可以是字符串、symbol 或其他类型,而值则具有一个已定义的 schema。
字符串键
你可以定义键为字符串、并为其值指定类型的 record。
示例(字符串键与数字值)
import { Schema } from "effect"
// Define a record schema with string keys and number values
//
// ┌─── $Record<typeof Schema.String, typeof Schema.Finite>
// ▼
const schema = Schema.Record(Schema.String, Schema.Finite)
// ┌─── { readonly [x: string]: number; }
// ▼
type Type = typeof schema.Type
Symbol 键
Record 也可以使用 symbol 作为键。
示例(Symbol 键与数字值)
import { Schema } from "effect"
// Define a record schema with symbol keys and number values
const schema = Schema.Record(Schema.Symbol, Schema.Finite)
// ┌─── { readonly [x: symbol]: number; }
// ▼
type Type = typeof schema.Type
字面量键的联合
使用字面量的联合可以把键限制在一组特定的值上。
示例(用字符串字面量作为键)
import { Schema } from "effect"
// Define a record schema where keys are limited
// to specific string literals ("a" or "b")
const schema = Schema.Record(
Schema.Union([Schema.Literal("a"), Schema.Literal("b")]),
Schema.Finite,
)
// ┌─── { readonly a: number; readonly b: number; }
// ▼
type Type = typeof schema.Type
模板字面量键
Record 可以使用模板字面量作为键,从而支持更复杂的键模式。
示例(模板字面量键与数字值)
import { Schema } from "effect"
// Define a record schema with keys that match
// the template literal pattern "a${string}"
const schema = Schema.Record(
Schema.TemplateLiteral([Schema.Literal("a"), Schema.String]),
Schema.Finite,
)
// ┌─── { readonly [x: `a${string}`]: number; }
// ▼
type Type = typeof schema.Type
细化后的键
你可以用额外的约束来细化键的类型。
示例(按最小长度过滤键)
import { Schema } from "effect"
// Define a record schema where keys are strings with a minimum length of 2
const schema = Schema.Record(
Schema.String.check(Schema.isMinLength(2)),
Schema.Finite,
)
// ┌─── { readonly [x: string]: number; }
// ▼
type Type = typeof schema.Type
对键的细化起的是过滤作用,而不会导致解码失败。 如果某个键不满足约束(例如模式或最小长度检查),它会被从解码输出中移除,而不是触发错误。
示例(不满足约束的键会被移除)
import { Schema } from "effect"
const schema = Schema.Record(
Schema.String.check(Schema.isMinLength(2)),
Schema.Finite,
)
console.log(Schema.decodeUnknownSync(schema)({ a: 1, bb: 2 }))
// Output: { bb: 2 } ("a" is removed because it is too short)
如果你希望在键不满足约束时让解码失败,可以把 onExcessProperty 设为 "error"。
示例(对无效键强制报错)
import { Schema } from "effect"
const schema = Schema.Record(
Schema.String.check(Schema.isMinLength(2)),
Schema.Finite,
)
console.log(
Schema.decodeUnknownSync(schema, { onExcessProperty: "error" })({
a: 1,
bb: 2,
}),
)
/*
throws:
SchemaError: { readonly [x: minLength(2)]: number }
└─ ["a"]
└─ is unexpected, expected: minLength(2)
*/
转换键
Schema.Record API 不支持对键 schema 做转换。
尝试对键应用转换会得到 Unsupported key schema 错误:
示例(尝试转换键)
import { Schema } from "effect"
const schema = Schema.Record(Schema.Trim, Schema.FiniteFromString)
/*
throws:
Error: Unsupported key schema
schema (Transformation): Trim
*/
存在这一限制是因为:如果多个键在转换后映射到同一个值,转换就会产生冲突。为避免这些问题,键的转换必须由用户显式处理。
要修改 record 的键,你必须在 Schema.Record 之外应用转换。
一种常见做法是用 Schema.decodeTo 搭配 SchemaTransformation.transform,在解码过程中调整键。
示例(解码时修剪键)
import { Record, Schema, SchemaTransformation, identity } from "effect"
const schema = Schema.Record(Schema.String, Schema.FiniteFromString).pipe(
Schema.decodeTo(
// Define the output schema with transformed keys
Schema.Record(Schema.Trimmed, Schema.Finite),
SchemaTransformation.transform({
// Trim keys during decoding
decode: (record) => Record.mapKeys(record, (key) => key.trim()),
encode: identity,
}),
),
)
console.log(Schema.decodeUnknownSync(schema)({ " key1 ": "1", key2: "2" }))
// Output: { key1: 1, key2: 2 }
可变 Record
默认情况下,Schema.Record 生成的类型被标记为 readonly。
要创建可变 record 的 schema,可以使用 Schema.mutable 函数,它以浅层(shallow)方式让 record 类型可变。
示例(创建可变 Record 的 schema)
import { Schema } from "effect"
// Create a schema for a mutable record with string keys and number values
const schema = Schema.Record(Schema.String, Schema.mutableKey(Schema.Finite))
// ┌─── { [x: string]: number; }
// ▼
type Type = typeof schema.Type
暴露的值
你可以使用 key 和 value 属性访问 record schema 的 key 和 value 类型:
示例(访问键与值的类型)
import { Schema } from "effect"
const schema = Schema.Record(Schema.String, Schema.Finite)
// Accesses the key
//
// ┌─── typeof Schema.String
// ▼
const key = schema.key
// Accesses the value
//
// ┌─── typeof Schema.Finite
// ▼
const value = schema.value
Struct
属性签名
Schema.Struct 构造器为具有特定属性的对象定义 schema。
示例(定义 Struct schema)
这个示例为一个对象定义了 struct schema,该对象具有以下属性:
name:字符串age:数字
import { Schema } from "effect"
// ┌─── Schema.Struct<{
// │ name: typeof Schema.String;
// │ age: typeof Schema.Finite;
// │ }>
// ▼
const schema = Schema.Struct({
name: Schema.String,
age: Schema.Finite,
})
// The inferred TypeScript type from the schema
//
// ┌─── {
// │ readonly name: string;
// │ readonly age: number;
// │ }
// ▼
type Type = typeof schema.Type
Schema.Struct({}) 生成的 TypeScript 类型是 {},并接受任何非 nullish 的值。它只拒绝 null 和 undefined。
索引签名
使用 Schema.StructWithRest 可以把一个 struct 与一个或多个表示索引签名的 record 组合起来。
示例(添加索引签名)
import { Schema } from "effect"
// Define a struct with a specific property "a"
// and an index signature allowing additional properties
const schema = Schema.StructWithRest(
// Defined properties
Schema.Struct({ a: Schema.Finite }),
// Index signature: allows additional string keys with number values
[Schema.Record(Schema.String, Schema.Finite)],
)
// The inferred TypeScript type:
//
// ┌─── {
// │ readonly [x: string]: number;
// │ readonly a: number;
// │ }
// ▼
type Type = typeof schema.Type
多个索引签名
每种键类型(string 或 symbol)只能定义一个索引签名。不允许定义多个同类型的索引签名。
示例(合法的多个索引签名)
import { Schema } from "effect"
// Define a struct with a fixed property "a"
// and valid index signatures for both strings and symbols
const schema = Schema.StructWithRest(Schema.Struct({ a: Schema.Finite }), [
// String index signature
Schema.Record(Schema.String, Schema.Finite),
// Symbol index signature
Schema.Record(Schema.Symbol, Schema.Finite),
])
// The inferred TypeScript type:
//
// ┌─── {
// │ readonly [x: string]: number;
// │ readonly [x: symbol]: number;
// │ readonly a: number;
// │ }
// ▼
type Type = typeof schema.Type
定义多个同一种键类型(string 或 symbol)的索引签名会导致错误。
示例(非法的多个索引签名)
import { Schema } from "effect"
Schema.StructWithRest(
Schema.Struct({ a: Schema.Finite }),
// Attempting to define multiple string index signatures
[
Schema.Record(Schema.String, Schema.Finite),
Schema.Record(Schema.String, Schema.Boolean),
],
)
/*
throws:
Error: Duplicate index signature
details: string index signature
*/
冲突的索引签名
在定义带索引签名的 schema 时,如果某个固定属性的类型与索引签名允许的值类型不同,就会产生冲突。 这可能导致意外的 TypeScript 行为。
示例(冲突的索引签名)
import { Schema } from "effect"
// Attempting to define a struct with a conflicting index signature
// - The fixed property "a" is a string
// - The index signature requires all values to be numbers
const schema = Schema.StructWithRest(Schema.Struct({ a: Schema.String }), [
Schema.Record(Schema.String, Schema.Finite),
])
// ❌ Incorrect TypeScript type:
//
// ┌─── {
// │ readonly [x: string]: number;
// │ readonly a: string;
// │ }
// ▼
type Type = typeof schema.Type
TypeScript 编译器在手动定义该类型时会把它标记为错误:
// @errors: 2411
// This type is invalid because the index signature
// conflicts with the fixed property `a`
type Test = {
readonly a: string
readonly [x: string]: number
}
出现这种情况是因为 TypeScript 不允许索引签名与固定属性相矛盾。
冲突索引签名的变通方案
在使用 schema 时,如果某个固定属性与索引签名允许的值类型不同,就可能发生冲突。这种情况常常出现在处理不遵循严格 TypeScript 约定的外部 API 时。
为避免冲突,你可以把固定属性与索引属性分开,并把它们作为 schema 中两个独立的部分来处理。
示例(提取固定属性与索引属性)
考虑这样一个对象:
"a"是类型为string的固定属性。- 其他所有键都存储数字,这与
"a"冲突。
// @errors: 2411
// This type is invalid because the index signature
// conflicts with the fixed property `a`
type Test = {
a: string
[x: string]: number
}
为避免这个问题,我们可以把这些属性拆成两个不同的类型:
// Fixed properties schema
type FixedProperties = {
readonly a: string
}
// Index signature properties schema
type IndexSignatureProperties = {
readonly [x: string]: number
}
// The final output groups both properties in a tuple
type OutputData = readonly [FixedProperties, IndexSignatureProperties]
通过 Schema.decodeTo 和 SchemaTransformation.transform,你可以在解码前对输入数据做预处理。这种方式能确保固定属性与索引签名属性被独立处理。
import { Schema, SchemaTransformation } from "effect"
// Define a schema for the fixed property "a"
const FixedProperties = Schema.Struct({
a: Schema.String,
})
// Define a schema for index signature properties
const IndexSignatureProperties = Schema.Record(
// Exclude keys that are already present in FixedProperties
Schema.String.check(
Schema.makeFilter(
(key) => !Object.keys(FixedProperties.fields).includes(key),
),
),
Schema.Finite,
)
// Create a schema that duplicates an object into two parts
const Duplicate = Schema.ObjectKeyword.pipe(
Schema.decodeTo(
Schema.Tuple([Schema.ObjectKeyword, Schema.ObjectKeyword]),
SchemaTransformation.transform({
// Create a tuple containing the input twice
decode: (a) => [a, a] as const,
// Merge both parts back when encoding
encode: ([a, b]) => ({ ...a, ...b }),
}),
),
)
const Result = Duplicate.pipe(
Schema.decodeTo(
Schema.Tuple([FixedProperties, IndexSignatureProperties]).annotate({
parseOptions: { onExcessProperty: "ignore" },
}),
),
)
// Decoding: Separates fixed and indexed properties
console.log(Schema.decodeUnknownSync(Result)({ a: "a", b: 1, c: 2 }))
// Output: [ { a: 'a' }, { b: 1, c: 2 } ]
// Encoding: Combines them back into an object
console.log(Schema.encodeSync(Result)([{ a: "a" }, { b: 1, c: 2 }]))
// Output: { a: 'a', b: 1, c: 2 }
暴露的值
你可以使用 fields 和 records 属性访问 struct schema 的字段与 record:
示例(访问字段与 record)
import { Schema } from "effect"
const schema = Schema.StructWithRest(Schema.Struct({ a: Schema.Finite }), [
Schema.Record(Schema.String, Schema.Finite),
])
// Accesses the fields
//
// ┌─── { readonly a: typeof Schema.Finite; }
// ▼
const fields = schema.schema.fields
// Accesses the records
//
// ┌─── readonly [Schema.$Record<typeof Schema.String, typeof Schema.Finite>]
// ▼
const records = schema.records
可变 Struct
默认情况下,Schema.Struct 生成的类型中,属性被标记为 readonly。
要为 struct 创建可变版本,可以使用 Schema.mutable 函数,它以浅层方式让属性变为可变。
示例(创建可变 Struct Schema)
import { Schema, Struct } from "effect"
const schema = Schema.Struct({ a: Schema.String, b: Schema.Finite }).mapFields(
Struct.map(Schema.mutableKey),
)
// ┌─── { a: string; b: number; }
// ▼
type Type = typeof schema.Type
带标签的结构体
在 TypeScript 中,标签有助于增强类型判别与模式匹配,它提供了一种简单而强大的方式来定义和识别不同的数据类型。
什么是标签?
标签是添加到数据结构上的一个字面量值,常用于 struct 中,用来区分带标签联合里的各种对象类型或变体。这个字面量充当判别属性,让人能更轻松、更高效地正确处理不同类型的数据。
使用 tag 构造器
Schema.tag 构造器专门用于创建一个持有特定字面量值的属性签名,作为对象类型的判别属性。
示例(定义带标签的结构体)
import { Schema } from "effect"
const User = Schema.Struct({
_tag: Schema.tag("User"),
name: Schema.String,
age: Schema.Finite,
})
// ┌─── { readonly _tag: "User"; readonly name: string; readonly age: number; }
// ▼
type Type = typeof User.Type
console.log(User.make({ name: "John", age: 44 }))
/*
Output:
{ _tag: 'User', name: 'John', age: 44 }
*/
在上面的例子中,Schema.tag("User") 为 User struct schema 附加了一个 _tag 属性,从而把该 struct 类型的对象标记为 “User”。
使用 make 方法创建新实例时,这个标签会被自动应用,从而简化对象创建并确保标签一致。
用 TaggedStruct 简化带标签的结构体
Schema.TaggedStruct 构造器把标签直接整合进 struct 定义中,从而简化了创建带标签 struct 的过程。这种方式为构建内嵌判别属性的数据结构提供了更清晰、更具声明性的途径。
示例(使用 TaggedStruct 简化带标签的结构体)
import { Schema } from "effect"
const User = Schema.TaggedStruct("User", {
name: Schema.String,
age: Schema.Finite,
})
// `_tag` is automatically applied when constructing an instance
console.log(User.make({ name: "John", age: 44 }))
// Output: { _tag: 'User', name: 'John', age: 44 }
// `_tag` is required when decoding from an unknown source
console.log(Schema.decodeUnknownSync(User)({ name: "John", age: 44 }))
/*
throws:
SchemaError: { readonly _tag: "User"; readonly name: string; readonly age: number }
└─ ["_tag"]
└─ is missing
*/
在这个示例中:
- 使用
make构造实例时,_tag属性是可选的,schema 会自动应用它。 - 解码未知数据时,必须提供
_tag,以确保正确识别类型。这种「实例构造」与「解码」之间的区别很有用:既保留了标签作为类型判别属性的角色,又简化了实例创建。
如果你需要 _tag 在解码时也被自动应用,可以创建一个定制版的 Schema.TaggedStruct:
示例(定制 TaggedStruct,在解码时应用 _tag)
import type { SchemaAST } from "effect"
import { Schema } from "effect"
const TaggedStruct = <
Tag extends SchemaAST.LiteralValue,
Fields extends Schema.Struct.Fields,
>(
tag: Tag,
fields: Fields,
) =>
Schema.Struct({
_tag: Schema.tagDefaultOmit(tag),
...fields,
})
const User = TaggedStruct("User", {
name: Schema.String,
age: Schema.Finite,
})
console.log(User.make({ name: "John", age: 44 }))
// Output: { _tag: 'User', name: 'John', age: 44 }
console.log(Schema.decodeUnknownSync(User)({ name: "John", age: 44 }))
// Output: { _tag: 'User', name: 'John', age: 44 }
多个标签
虽然一个主标签通常就足够了,但 TypeScript 允许你为更复杂的数据结构需求定义多个标签。下面是一个在单个 struct 中使用多个标签的示例:
示例(为一个 struct 添加多个标签)
这个示例定义了一个产品 schema,其中包含一个主标签("Product")和一个额外的分类标签("Electronics"),为数据结构增加了更细致的区分。
import { Schema } from "effect"
const Product = Schema.TaggedStruct("Product", {
category: Schema.tag("Electronics"),
name: Schema.String,
price: Schema.Finite,
})
// `_tag` and `category` are optional when creating an instance
console.log(Product.make({ name: "Smartphone", price: 999 }))
/*
Output:
{
_tag: 'Product',
category: 'Electronics',
name: 'Smartphone',
price: 999
}
*/
instanceOf
当你需要为通过 class 定义的自定义数据类型定义 schema 时,最方便、最快捷的方式是使用 Schema.instanceOf 构造器。
示例(使用 instanceOf 定义 schema)
import { Schema } from "effect"
// Define a custom class
class MyData {
constructor(readonly name: string) {}
}
// Create a schema for the class
const MyDataSchema = Schema.instanceOf(MyData)
// ┌─── MyData
// ▼
type Type = typeof MyDataSchema.Type
console.log(Schema.decodeUnknownSync(MyDataSchema)(new MyData("name")))
// Output: MyData { name: 'name' }
console.log(Schema.decodeUnknownSync(MyDataSchema)({ name: "name" }))
/*
throws:
SchemaError: Expected MyData
*/
Schema.instanceOf 构造器只是 Schema.declare API 的一个轻量包装,后者是 effect/Schema 中用于声明新自定义数据类型的原语。
私有构造器
注意,Schema.instanceOf 只能用于暴露了公开构造器的类。
如果你尝试把它用于因某种原因把构造器标记为 private 的类,会收到一个 TypeScript 错误:
示例(私有构造器导致的错误)
import { Schema } from "effect"
class MyData {
static make = (name: string) => new MyData(name)
private constructor(readonly name: string) {}
}
// @errors: 2345
const MyDataSchema = Schema.instanceOf(MyData)
在这种情况下,你无法使用 Schema.instanceOf,必须像下面这样依赖 Schema.declare:
示例(对私有构造器使用 Schema.declare)
import { Schema } from "effect"
class MyData {
static make = (name: string) => new MyData(name)
private constructor(readonly name: string) {}
}
const MyDataSchema = Schema.declare(
(input: unknown): input is MyData => input instanceof MyData,
).annotate({ identifier: "MyData" })
console.log(Schema.decodeUnknownSync(MyDataSchema)(MyData.make("name")))
// Output: MyData { name: 'name' }
console.log(Schema.decodeUnknownSync(MyDataSchema)({ name: "name" }))
/*
throws:
SchemaError: Expected MyData
*/
校验实例的字段
要校验类实例的字段,你可以使用过滤器(filter)。这种方式把实例校验与对实例字段的额外检查结合起来。
示例(为实例 schema 添加字段校验)
import { Result, Schema } from "effect"
class MyData {
constructor(readonly name: string) {}
}
const MyDataFields = Schema.Struct({
name: Schema.NonEmptyString,
})
// Define a schema for the class instance with additional field validation
const MyDataSchema = Schema.instanceOf(MyData).check(
Schema.makeFilter((a, _ast, options) => {
// Validate the fields of the instance
const result = Schema.decodeUnknownResult(MyDataFields)(a, options)
// Return undefined if validation succeeds, or the issue if it fails
return Result.isFailure(result) ? result.failure.issue : undefined
}),
)
const decodeTypeSync = Schema.decodeSync(Schema.toType(MyDataSchema))
// Example: Valid instance
console.log(decodeTypeSync(new MyData("John")))
// Output: MyData { name: 'John' }
// Example: Invalid instance (empty name)
console.log(decodeTypeSync(new MyData("")))
/*
throws:
SchemaError: { MyData | filter }
└─ Predicate refinement failure
└─ { readonly name: NonEmptyString }
└─ ["name"]
└─ NonEmptyString
└─ Predicate refinement failure
└─ Expected a non empty string
*/
挑选
使用 Struct.pick 搭配 Struct.mapFields,通过从已有 struct schema 中选取字段来创建新的 struct schema。
示例(从 struct 中挑选属性)
import { Schema, Struct } from "effect"
// Define a struct schema with properties "a", "b", and "c"
const MyStruct = Schema.Struct({
a: Schema.String,
b: Schema.Finite,
c: Schema.Boolean,
})
// Create a new schema that picks properties "a" and "c"
//
// ┌─── Struct<{
// | a: typeof Schema.String;
// | c: typeof Schema.Boolean;
// | }>
// ▼
const PickedSchema = MyStruct.mapFields(Struct.pick(["a", "c"]))
省略
使用 Struct.omit 搭配 Struct.mapFields,通过从已有 struct schema 中排除字段来创建新的 struct schema。
示例(从 struct 中省略属性)
import { Schema, Struct } from "effect"
// Define a struct schema with properties "a", "b", and "c"
const MyStruct = Schema.Struct({
a: Schema.String,
b: Schema.Finite,
c: Schema.Boolean,
})
// Create a new schema that omits property "b"
//
// ┌─── Schema.Struct<{
// | a: typeof Schema.String;
// | c: typeof Schema.Boolean;
// | }>
// ▼
const OmittedSchema = MyStruct.mapFields(Struct.omit(["b"]))
让属性可选
使用 Struct.map 搭配 Schema.optional,让 struct 中的每个字段都变为可选。
示例(让所有属性可选)
import { Schema, Struct } from "effect"
// Create a schema with an optional property "a"
const schema = Schema.Struct({ a: Schema.String }).mapFields(
Struct.map(Schema.optional),
)
// ┌─── { readonly a?: string | undefined; }
// ▼
type Type = typeof schema.Type
Schema.optional 会为每个字段类型加上 undefined。如果某个字段可以被省略,但一旦出现就必须包含其 schema 接受的值,请改用 Schema.optionalKey。
示例(定义一个精确的 partial schema)
import { Schema, Struct } from "effect"
// Create a schema with an optional property "a" without allowing undefined
const schema = Schema.Struct({
a: Schema.String,
}).mapFields(Struct.map(Schema.optionalKey))
// ┌─── { readonly a?: string; }
// ▼
type Type = typeof schema.Type
让属性必需
使用 Struct.map 搭配 Schema.requiredKey,让 struct 中的每个可选键都变为必需。
示例(让所有属性必需)
import { Schema, Struct } from "effect"
// Create a schema and make all properties required
const schema = Schema.Struct({
a: Schema.optionalKey(Schema.String),
b: Schema.optionalKey(Schema.Finite),
}).mapFields(Struct.map(Schema.requiredKey))
// ┌─── { readonly a: string; readonly b: number; }
// ▼
type Type = typeof schema.Type
在这个示例中,尽管 a 和 b 最初被定义为可选,现在它们都变成了必需。