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

基本用法

学习定义和使用基础 schema,包括原始类型、字面量、联合和 Struct,以实现有效的数据校验与转换。

原始类型

Schema 模块为常见的原始类型提供了内置 schema。

Schema等价的 TypeScript 类型
Schema.Stringstring
Schema.Finitenumber
Schema.Booleanboolean
Schema.BigIntbigint
Schema.Symbolsymbol
Schema.ObjectKeywordobject
Schema.Undefinedundefined
Schema.Voidvoid
Schema.Anyany
Schema.Unknownunknown
Schema.Nevernever

示例(使用原始类型 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 表示一种字面量类型。你可以用它们来指定某个类型必须具有的确切值。

字面量可以是以下类型:

  • string
  • number
  • boolean
  • null
  • bigint

示例(定义字面量 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.String
  • Schema.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,帮助你处理可能是 nullundefined 或两者兼有的值。

示例(创建可空 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 属性设置为 CircleSquare 两个 schema 共同的判别属性。随后 Shape schema 表示这两个类型的联合,让 TypeScript 能够根据 kind 的值推断出具体的形状。

把简单联合转换为可辨识联合

如果你从一个简单联合开始,并想把它转换为可辨识联合,可以为每个成员添加一个特殊属性。这样 TypeScript 就能根据判别属性的值自动推断出正确的类型。

示例(最初的简单联合)

例如,假设你定义了一个由 CircleSquare 组合而成、不带任何特殊属性的 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: {}
}
*/

暴露的值

你可以使用 elementsrest 属性访问元组 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
*/
Why Key Transformations Are Not Allowed

存在这一限制是因为:如果多个键在转换后映射到同一个值,转换就会产生冲突。为避免这些问题,键的转换必须由用户显式处理。

要修改 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

暴露的值

你可以使用 keyvalue 属性访问 record schema 的 keyvalue 类型:

示例(访问键与值的类型)

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
Empty Structs Allow Any Data

Schema.Struct({}) 生成的 TypeScript 类型是 {},并接受任何非 nullish 的值。它只拒绝 nullundefined

索引签名

使用 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

多个索引签名

每种键类型(stringsymbol)只能定义一个索引签名。不允许定义多个同类型的索引签名。

示例(合法的多个索引签名)

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

定义多个同一种键类型(stringsymbol)的索引签名会导致错误。

示例(非法的多个索引签名)

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.decodeToSchemaTransformation.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 }

暴露的值

你可以使用 fieldsrecords 属性访问 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

在这个示例中,尽管 ab 最初被定义为可选,现在它们都变成了必需。