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

Schema 变换

使用基于 schema 的变换来转换和处理数据,包括类型转换、校验以及自定义处理。

在处理 schema 时,变换非常重要。它让你可以把数据从一种类型转换为另一种类型。例如,你可以把字符串解析为数字,或者把日期字符串转换为 Date 对象。

Schema.transformSchema.transformOrFail 这两个函数帮助你连接两个 schema,从而在它们之间转换数据。

transform

Schema.transform 会取一个 schema(「源」)的输出,把它作为另一个 schema(「目标」)的输入,从而创建一个新的 schema。当你确信该变换总会成功时使用它;如果它可能失败,请改用 Schema.transformOrFail

理解输入与输出

“输出”与“输入”取决于你正在做什么(解码还是编码):

解码时:

  • 源 schema Schema<SourceType, SourceEncoded> 产出 SourceType
  • 目标 schema Schema<TargetType, TargetEncoded> 期望得到 TargetEncoded
  • 解码路径如下:SourceEncodedTargetType

如果 SourceTypeTargetEncoded 不同,你可以提供一个 decode 函数,把源 schema 的输出转换为目标 schema 的输入。

编码时:

  • 目标 schema Schema<TargetType, TargetEncoded> 产出 TargetEncoded
  • 源 schema Schema<SourceType, SourceEncoded> 期望得到 SourceType
  • 编码路径如下:TargetTypeSourceEncoded

如果 TargetEncodedSourceType 不同,你可以提供一个 encode 函数,把目标 schema 的输出转换为源 schema 的输入。

组合两个原始 schema

在这个示例中,我们从一个接受 "on""off" 的 schema 出发,把它转换为一个布尔 schema。decode 函数把 "on" 变为 true、把 "off" 变为 falseencode 函数则执行相反的操作。这样我们就得到了一个 Schema<boolean, "on" | "off">

示例(把字符串转换为布尔值)

import { Schema } from "effect"

// Convert "on"/"off" to boolean and back
const BooleanFromString = Schema.transform(
  // Source schema: "on" or "off"
  Schema.Literal("on", "off"),
  // Target schema: boolean
  Schema.Boolean,
  {
    // optional but you get better error messages from TypeScript
    strict: true,
    // Transformation to convert the output of the
    // source schema ("on" | "off") into the input of the
    // target schema (boolean)
    decode: (literal) => literal === "on", // Always succeeds here
    // Reverse transformation
    encode: (bool) => (bool ? "on" : "off"),
  },
)

//     ┌─── "on" | "off"
//     ▼
type Encoded = typeof BooleanFromString.Encoded

//     ┌─── boolean
//     ▼
type Type = typeof BooleanFromString.Type

console.log(Schema.decodeUnknownSync(BooleanFromString)("on"))
// Output: true

上面的 decode 函数本身永远不会失败。不过,如果输入不符合源 schema,整个解码过程仍然可能失败。例如,如果你提供的是 "wrong" 而不是 "on""off",源 schema 会在调用 decode 之前就失败。

示例(处理无效输入)

import { Schema } from "effect"

// Convert "on"/"off" to boolean and back
const BooleanFromString = Schema.transform(
  Schema.Literal("on", "off"),
  Schema.Boolean,
  {
    strict: true,
    decode: (s) => s === "on",
    encode: (bool) => (bool ? "on" : "off"),
  },
)

// Providing input not allowed by the source schema
Schema.decodeUnknownSync(BooleanFromString)("wrong")
/*
throws:
ParseError: ("on" | "off" <-> boolean)
└─ Encoded side transformation failure
   └─ "on" | "off"
      ├─ Expected "on", actual "wrong"
      └─ Expected "off", actual "wrong"
*/

组合两个变换 schema

下面这个示例中,源 schema 与目标 schema 都会对各自的数据做变换:

  • 源 schema 是 Schema.NumberFromString,即 Schema<number, string>
  • 目标 schema 是 BooleanFromString(上面已定义),即 Schema<boolean, "on" | "off">

这个示例涉及四种类型,需要进行两次转换:

  • 解码时,把 number 转换为 "on" | "off"。例如,把任何正数都视为 "on"
  • 编码时,把 "on" | "off" 转换回 number。例如,把 "on" 视为 1,把 "off" 视为 -1

通过组合这些变换,我们得到一个 schema:它能把字符串解码为布尔值,也能把布尔值编码回字符串。得到的 schema 是 Schema<boolean, string>

示例(组合两个变换 schema)

import { Schema } from "effect"

// Convert "on"/"off" to boolean and back
const BooleanFromString = Schema.transform(
  Schema.Literal("on", "off"),
  Schema.Boolean,
  {
    strict: true,
    decode: (s) => s === "on",
    encode: (bool) => (bool ? "on" : "off"),
  },
)

const BooleanFromNumericString = Schema.transform(
  // Source schema: Convert string -> number
  Schema.NumberFromString,
  // Target schema: Convert "on"/"off" -> boolean
  BooleanFromString,
  {
    strict: true,
    // If number is positive, use "on", otherwise "off"
    decode: (n) => (n > 0 ? "on" : "off"),
    // If boolean is "on", use 1, otherwise -1
    encode: (bool) => (bool === "on" ? 1 : -1),
  },
)

//     ┌─── string
//     ▼
type Encoded = typeof BooleanFromNumericString.Encoded

//     ┌─── boolean
//     ▼
type Type = typeof BooleanFromNumericString.Type

console.log(Schema.decodeUnknownSync(BooleanFromNumericString)("100"))
// Output: true

示例(把数组转换为 ReadonlySet)

在这个示例中,我们把一个数组转换为 ReadonlySetdecode 函数接收一个数组并创建一个新的 ReadonlySetencode 函数则把 set 转换回数组。我们还提供了数组元素的 schema,以便它们得到正确的校验。

import { Schema } from "effect"

// This function builds a schema that converts between a readonly array
// and a readonly set of items
const ReadonlySetFromArray = <A, I, R>(
  itemSchema: Schema.Schema<A, I, R>,
): Schema.Schema<ReadonlySet<A>, ReadonlyArray<I>, R> =>
  Schema.transform(
    // Source schema: array of items
    Schema.Array(itemSchema),
    // Target schema: readonly set of items
    // **IMPORTANT** We use `Schema.typeSchema` here to obtain the schema
    // of the items to avoid decoding the elements twice
    Schema.ReadonlySetFromSelf(Schema.typeSchema(itemSchema)),
    {
      strict: true,
      decode: (items) => new Set(items),
      encode: (set) => Array.from(set.values()),
    },
  )

const schema = ReadonlySetFromArray(Schema.String)

//     ┌─── readonly string[]
//     ▼
type Encoded = typeof schema.Encoded

//     ┌─── ReadonlySet<string>
//     ▼
type Type = typeof schema.Type

console.log(Schema.decodeUnknownSync(schema)(["a", "b", "c"]))
// Output: Set(3) { 'a', 'b', 'c' }

console.log(Schema.encodeSync(schema)(new Set(["a", "b", "c"])))
// Output: [ 'a', 'b', 'c' ]
Why Schema.typeSchema is used

请注意,为了定义目标 schema,我们使用了 Schema.typeSchema。这是因为 元素的解码/编码已经由 from schema 处理了: 即 Schema.Array(itemSchema),从而避免了重复解码。

非严格选项

在某些情况下,严格的类型检查会在数据变换期间引发问题,尤其是当类型在某些特定变换中略有差异时。为应对这类情形,Schema.transform 提供了 strict: false 选项,它会放宽类型约束,允许更灵活的变换。

示例(创建一个限制范围的构造器)

让我们来看这样一个场景:你需要定义一个构造器 clamp,用来确保数字落在特定范围内。该函数返回一个 schema,它会把数字限制在指定的最小值和最大值范围内:

import { Schema, Number } from "effect"

const clamp =
  (minimum: number, maximum: number) =>
  <A extends number, I, R>(self: Schema.Schema<A, I, R>) =>
    Schema.transform(
      // Source schema
      self,
      // Target schema: filter based on min/max range
      self.pipe(
        Schema.typeSchema,
        Schema.filter((a) => a <= minimum || a >= maximum),
      ),
      // @errors: 2345
      {
        strict: true,
        // Clamp the number within the specified range
        decode: (a) => Number.clamp(a, { minimum, maximum }),
        encode: (a) => a,
      },
    )

在这个示例中,Number.clamp 返回的 number 可能不会被识别为具体的 A 类型,这在严格检查下会导致类型不匹配。

有两种方式可以解决这个问题:

  1. 使用类型断言: 添加类型转换可以强制把返回类型当作类型 A 处理:

    decode: (a) => Number.clamp(a, { minimum, maximum }) as A
  2. 使用非严格选项: 在变换选项中设置 strict: false,可以让 schema 绕过 TypeScript 的部分类型检查规则,从而容纳这种类型差异:

    import { Schema, Number } from "effect"
    
    const clamp =
      (minimum: number, maximum: number) =>
      <A extends number, I, R>(self: Schema.Schema<A, I, R>) =>
        Schema.transform(
          self,
          self.pipe(
            Schema.typeSchema,
            Schema.filter((a) => a >= minimum && a <= maximum),
          ),
          {
            strict: false,
            decode: (a) => Number.clamp(a, { minimum, maximum }),
            encode: (a) => a,
          },
        )

transformOrFail

Schema.transform 函数适用于不会出错的变换, 而 Schema.transformOrFail 函数则面向更复杂的场景:在这些场景中,变换 可能在解码或编码阶段失败

这个函数让解码/编码函数既可以返回成功结果,也可以返回错误, 因此在校验和处理那些未必总是符合预期格式的数据时特别有用。

错误处理

Schema.transformOrFail 函数借助 ParseResult 模块来管理可能出现的错误:

构造器说明
ParseResult.succeed表示变换成功,未发生任何错误。
ParseResult.fail表示变换失败,并根据所提供的 ParseIssue 创建一个新的 ParseError

此外,ParseResult 模块还提供了用于处理各种解析问题类型的构造器,例如:

解析问题类型说明
Type表示类型不匹配错误。
Missing在缺少必填字段时使用。
Unexpected用于 schema 中不允许出现的意外字段。
Forbidden标记解码或编码操作被 schema 禁止。
Pointer指向数据中发生问题的具体位置。
Refinement在值不满足特定 refinement 或约束时使用。
Transformation标记从一种类型变换为另一种类型时出现的问题。
Composite表示复合错误,把多个问题合并为一个,便于对错误分组。

这些工具支持细致而具体的错误处理,从而提升了数据处理操作的可靠性。

示例(把字符串转换为数字)

Schema.transformOrFail 的一个常见用例是把数字的字符串表示转换为真正的数值类型。在处理用户输入或来自外部来源的数据时,这种场景很典型。

import { ParseResult, Schema } from "effect"

export const NumberFromString = Schema.transformOrFail(
  // Source schema: accepts any string
  Schema.String,
  // Target schema: expects a number
  Schema.Number,
  {
    // optional but you get better error messages from TypeScript
    strict: true,
    decode: (input, options, ast) => {
      const parsed = parseFloat(input)
      // If parsing fails (NaN), return a ParseError with a custom error
      if (isNaN(parsed)) {
        return ParseResult.fail(
          // Create a Type Mismatch error
          new ParseResult.Type(
            // Provide the schema's abstract syntax tree for context
            ast,
            // Include the problematic input
            input,
            // Optional custom error message
            "Failed to convert string to number",
          ),
        )
      }
      return ParseResult.succeed(parsed)
    },
    encode: (input, options, ast) => ParseResult.succeed(input.toString()),
  },
)

//     ┌─── string
//     ▼
type Encoded = typeof NumberFromString.Encoded

//     ┌─── number
//     ▼
type Type = typeof NumberFromString.Type

console.log(Schema.decodeUnknownSync(NumberFromString)("123"))
// Output: 123

console.log(Schema.decodeUnknownSync(NumberFromString)("-"))
/*
throws:
ParseError: (string <-> number)
└─ Transformation process failure
   └─ Failed to convert string to number
*/

decodeencode 函数不仅会接收要变换的值(input),还会接收用户在使用所得 schema 时设置的 parse 选项,以及 ast —— 它表示你正在变换的 schema 的底层定义。

异步变换

在现代应用中,尤其是那些需要与外部 API 交互的应用,你可能需要异步地转换数据。Schema.transformOrFail 通过允许你返回一个 Effect 来支持异步变换。

示例(通过 API 调用校验数据)

假设你需要通过发起 API 调用来校验一个人的 ID,可以这样实现:

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

// Define a function to make API requests
const get = (url: string): Effect.Effect<unknown, Error> =>
  Effect.tryPromise({
    try: () =>
      fetch(url).then((res) => {
        if (res.ok) {
          return res.json() as Promise<unknown>
        }
        throw new Error(String(res.status))
      }),
    catch: (e) => new Error(String(e)),
  })

// Create a branded schema for a person's ID
const PeopleId = Schema.String.pipe(Schema.brand("PeopleId"))

// Define a schema with async transformation
const PeopleIdFromString = Schema.transformOrFail(Schema.String, PeopleId, {
  strict: true,
  decode: (s, _, ast) =>
    // Make an API call to validate the ID
    Effect.mapBoth(get(`https://swapi.dev/api/people/${s}`), {
      // Error handling for failed API call
      onFailure: (e) => new ParseResult.Type(ast, s, e.message),
      // Return the ID if the API call succeeds
      onSuccess: () => s,
    }),
  encode: ParseResult.succeed,
})

//     ┌─── string
//     ▼
type Encoded = typeof PeopleIdFromString.Encoded

//     ┌─── string & Brand<"PeopleId">
//     ▼
type Type = typeof PeopleIdFromString.Type

//     ┌─── never
//     ▼
type Context = typeof PeopleIdFromString.Context

// Run a successful decode operation
Effect.runPromiseExit(Schema.decodeUnknown(PeopleIdFromString)("1")).then(
  console.log,
)
/*
Output:
{ _id: 'Exit', _tag: 'Success', value: '1' }
*/

// Run a decode operation that will fail
Effect.runPromiseExit(Schema.decodeUnknown(PeopleIdFromString)("fail")).then(
  console.log,
)
/*
Output:
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: {
    _id: 'Cause',
    _tag: 'Fail',
    failure: {
      _id: 'ParseError',
      message: '(string <-> string & Brand<"PeopleId">)\n' +
        '└─ Transformation process failure\n' +
        '   └─ Error: 404'
    }
  }
}
*/

声明依赖

当你的变换依赖外部 service 时,可以在 decodeencode 函数中注入这些 service。这些依赖随后会被记录在 schema 的 Requirements 通道中:

Schema<Type, Encoded, Requirements>

示例(用 service 校验数据)

import { Context, Effect, Schema, ParseResult, Layer } from "effect"

// Define a Validation service for dependency injection
class Validation extends Context.Tag("Validation")<
  Validation,
  {
    readonly validatePeopleid: (s: string) => Effect.Effect<void, Error>
  }
>() {}

// Create a branded schema for a person's ID
const PeopleId = Schema.String.pipe(Schema.brand("PeopleId"))

// Transform a string into a validated PeopleId,
// using an external validation service
const PeopleIdFromString = Schema.transformOrFail(Schema.String, PeopleId, {
  strict: true,
  decode: (s, _, ast) =>
    // Asynchronously validate the ID using the injected service
    Effect.gen(function* () {
      // Access the validation service
      const validator = yield* Validation
      // Use service to validate ID
      yield* validator.validatePeopleid(s)
      return s
    }).pipe(Effect.mapError((e) => new ParseResult.Type(ast, s, e.message))),
  encode: ParseResult.succeed, // Encode by simply returning the string
})

//     ┌─── string
//     ▼
type Encoded = typeof PeopleIdFromString.Encoded

//     ┌─── string & Brand<"PeopleId">
//     ▼
type Type = typeof PeopleIdFromString.Type

//     ┌─── Validation
//     ▼
type Context = typeof PeopleIdFromString.Context

// Layer to provide a successful validation service
const SuccessTest = Layer.succeed(Validation, {
  validatePeopleid: (_) => Effect.void,
})

// Run a successful decode operation
Effect.runPromiseExit(
  Schema.decodeUnknown(PeopleIdFromString)("1").pipe(
    Effect.provide(SuccessTest),
  ),
).then(console.log)
/*
Output:
{ _id: 'Exit', _tag: 'Success', value: '1' }
*/

// Layer to provide a failing validation service
const FailureTest = Layer.succeed(Validation, {
  validatePeopleid: (_) => Effect.fail(new Error("404")),
})

// Run a decode operation that will fail
Effect.runPromiseExit(
  Schema.decodeUnknown(PeopleIdFromString)("fail").pipe(
    Effect.provide(FailureTest),
  ),
).then(console.log)
/*
Output:
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: {
    _id: 'Cause',
    _tag: 'Fail',
    failure: {
      _id: 'ParseError',
      message: '(string <-> string & Brand<"PeopleId">)\n' +
        '└─ Transformation process failure\n' +
        '   └─ Error: 404'
    }
  }
}
*/

禁止编码的单向变换

在某些情况下,把值编码回它原本的形式可能没有意义,也可能并不希望如此。你可以用 Schema.transformOrFail 定义一个单向变换,并在编码过程中显式返回 Forbidden 解析错误。这样就保证了:一旦某个值被变换,就无法再还原为它原本的形式。

示例(带禁止编码的密码哈希)

设想这样一个场景:你需要对用户的明文密码做哈希,以便安全存储。关键在于,哈希后的密码不能被逆向还原为明文。通过 Schema.transformOrFail,你可以强制实施这一限制,从而确保从明文到哈希密码的单向变换。

import { Schema, ParseResult, Redacted } from "effect"
import { createHash } from "node:crypto"

// Define a schema for plain text passwords
// with a minimum length requirement
const PlainPassword = Schema.String.pipe(
  Schema.minLength(6),
  Schema.brand("PlainPassword", { identifier: "PlainPassword" }),
)

// Define a schema for hashed passwords as a separate branded type
const HashedPassword = Schema.String.pipe(
  Schema.brand("HashedPassword", { identifier: "HashedPassword" }),
)

// Define a one-way transformation from plain passwords to hashed passwords
export const PasswordHashing = Schema.transformOrFail(
  PlainPassword,
  // Wrap the output in Redacted for added safety
  Schema.RedactedFromSelf(HashedPassword),
  {
    strict: true,
    // Decode: Transform a plain password into a hashed password
    decode: (plainPassword) => {
      const hash = createHash("sha256").update(plainPassword).digest("hex")
      // Wrap the hash in Redacted
      return ParseResult.succeed(Redacted.make(hash))
    },
    // Encode: Forbid reversing the hashed password back to plain text
    encode: (hashedPassword, _, ast) =>
      ParseResult.fail(
        new ParseResult.Forbidden(
          ast,
          hashedPassword,
          "Encoding hashed passwords back to plain text is forbidden.",
        ),
      ),
  },
)

//     ┌─── string
//     ▼
type Encoded = typeof PasswordHashing.Encoded

//     ┌─── Redacted<string & Brand<"HashedPassword">>
//     ▼
type Type = typeof PasswordHashing.Type

// Example: Decoding a plain password into a hashed password
console.log(Schema.decodeUnknownSync(PasswordHashing)("myPlainPassword123"))
// Output: <redacted>

// Example: Attempting to encode a hashed password back to plain text
console.log(
  Schema.encodeUnknownSync(PasswordHashing)(Redacted.make("2ef2b7...")),
)
/*
throws:
ParseError: (PlainPassword <-> Redacted(<redacted>))
└─ Transformation process failure
   └─ (PlainPassword <-> Redacted(<redacted>))
      └─ Encoding hashed passwords back to plain text is forbidden.
*/

组合

在复杂应用中,经常需要组合并复用 schema,而 Schema.compose 组合子提供了一种高效的做法。借助 Schema.compose,你可以把两个 schema —— Schema<B, A, R1>Schema<C, B, R2> —— 串联成单个 schema Schema<C, A, R1 | R2>

示例(组合 schema,把带分隔符的字符串解析为数字)

import { Schema } from "effect"

// Schema to split a string by commas into an array of strings
//
//     ┌─── Schema<readonly string[], string, never>
//     ▼
const schema1 = Schema.asSchema(Schema.split(","))

// Schema to convert an array of strings to an array of numbers
//
//     ┌─── Schema<readonly number[], readonly string[], never>
//     ▼
const schema2 = Schema.asSchema(Schema.Array(Schema.NumberFromString))

// Composed schema that takes a string, splits it by commas,
// and converts the result into an array of numbers
//
//     ┌─── Schema<readonly number[], string, never>
//     ▼
const ComposedSchema = Schema.asSchema(Schema.compose(schema1, schema2))

非严格选项

在组合 schema 时,你可能会遇到某个 schema 的输出与下一个 schema 的输入并不完全匹配的情况。例如,你有 Schema<R1, A, B>Schema<R2, C, D>,而 CB 不同。要处理这类情况,可以用 { strict: false } 选项放宽类型约束。

示例(在组合中使用非严格选项)

import { Schema } from "effect"

// Without the `strict: false` option,
// this composition raises a TypeScript error
Schema.compose(
  // @errors: 2769
  Schema.Union(Schema.Null, Schema.Literal("0")),
  Schema.NumberFromString,
)

// Use `strict: false` to allow type flexibility
Schema.compose(
  Schema.Union(Schema.Null, Schema.Literal("0")),
  Schema.NumberFromString,
  {
    strict: false,
  },
)

带副作用的过滤器

Schema.filterEffect 函数支持那些需要异步或动态场景的校验,因此适用于校验过程中带有副作用的情况,例如网络请求或数据库查询。对于简单的同步校验,请参阅 Schema.filter

示例(异步校验用户名)

import { Effect, Schema } from "effect"

// Mock async function to validate a username
async function validateUsername(username: string) {
  return Promise.resolve(username === "gcanti")
}

// Define a schema with an effectful filter
const ValidUsername = Schema.String.pipe(
  Schema.filterEffect((username) =>
    Effect.promise(() =>
      // Validate the username asynchronously,
      // returning an error message if invalid
      validateUsername(username).then((valid) => valid || "Invalid username"),
    ),
  ),
).annotations({ identifier: "ValidUsername" })

Effect.runPromise(Schema.decodeUnknown(ValidUsername)("xxx")).then(console.log)
/*
ParseError: ValidUsername
└─ Transformation process failure
   └─ Invalid username
*/

字符串转换

split

按指定的分隔符把字符串拆分为子字符串数组。

示例(按逗号拆分字符串)

import { Schema } from "effect"

const schema = Schema.split(",")

const decode = Schema.decodeUnknownSync(schema)

console.log(decode("")) // [""]
console.log(decode(",")) // ["", ""]
console.log(decode("a,")) // ["a", ""]
console.log(decode("a,b")) // ["a", "b"]

Trim

去掉字符串开头和结尾的空白字符。

示例(去除空白字符)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.Trim)

console.log(decode("a")) // "a"
console.log(decode(" a")) // "a"
console.log(decode("a ")) // "a"
console.log(decode(" a ")) // "a"
Trimmed Check

如果你想找一个用于检查字符串是否已去除首尾空白的组合子,可以看看 Schema.trimmed 过滤器。

Lowercase

把字符串转换为小写。

示例(转换为小写)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.Lowercase)

console.log(decode("A")) // "a"
console.log(decode(" AB")) // " ab"
console.log(decode("Ab ")) // "ab "
console.log(decode(" ABc ")) // " abc "
Lowercase And Lowercased

如果你想找一个用于检查字符串是否为小写的组合子,可以看看 Schema.Lowercased schema 或 Schema.lowercased 过滤器。

Uppercase

把字符串转换为大写。

示例(转换为大写)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.Uppercase)

console.log(decode("a")) // "A"
console.log(decode(" ab")) // " AB"
console.log(decode("aB ")) // "AB "
console.log(decode(" abC ")) // " ABC "
Uppercase And Uppercased

如果你想找一个用于检查字符串是否为大写的组合子,可以看看 Schema.Uppercased schema 或 Schema.uppercased 过滤器。

Capitalize

把字符串的第一个字符转换为大写。

示例(把字符串首字母大写)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.Capitalize)

console.log(decode("aa")) // "Aa"
console.log(decode(" ab")) // " ab"
console.log(decode("aB ")) // "AB "
console.log(decode(" abC ")) // " abC "
Capitalize And Capitalized

如果你想找一个用于检查字符串首字母是否为大写的组合子,可以看看 Schema.Capitalized schema 或 Schema.capitalized 过滤器。

Uncapitalize

把字符串的第一个字符转换为小写。

示例(把字符串首字母小写)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.Uncapitalize)

console.log(decode("AA")) // "aA"
console.log(decode(" AB")) // " AB"
console.log(decode("Ab ")) // "ab "
console.log(decode(" AbC ")) // " AbC "
Uncapitalize And Uncapitalized

如果你想找一个用于检查字符串首字母是否为小写的组合子,可以看看 Schema.Uncapitalized schema 或 Schema.uncapitalized 过滤器。

parseJson

Schema.parseJson 构造函数提供了一种方法:借助 JSON.parse 的底层能力把 JSON 字符串转换为 unknown 类型。它在编码时还会使用 JSON.stringify

示例(解析 JSON 字符串)

import { Schema } from "effect"

const schema = Schema.parseJson()
const decode = Schema.decodeUnknownSync(schema)

// Parse valid JSON strings
console.log(decode("{}")) // Output: {}
console.log(decode(`{"a":"b"}`)) // Output: { a: "b" }

// Attempting to decode an empty string results in an error
decode("")
/*
throws:
ParseError: (JsonString <-> unknown)
└─ Transformation process failure
   └─ Unexpected end of JSON input
*/

若要进一步约束 JSON 解析的结果,你可以给 Schema.parseJson 构造函数传入一个 schema。这个 schema 会校验解析出的 JSON 是否符合特定结构。

示例(带结构校验的 JSON 解析)

在这个例子中,Schema.parseJson 使用一个 struct schema 来确保解析出的 JSON 是一个带有数值属性 a 的对象。这为解析出的数据加上了校验,确认它符合预期的结构。

import { Schema } from "effect"

//     ┌─── SchemaClass<{ readonly a: number; }, string, never>
//     ▼
const schema = Schema.parseJson(Schema.Struct({ a: Schema.Number }))

StringFromBase64

把 base64(RFC4648)编码的字符串解码为 UTF-8 字符串。

示例(解码 Base64)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.StringFromBase64)

console.log(decode("Zm9vYmFy"))
// Output: "foobar"

StringFromBase64Url

把 base64(URL)编码的字符串解码为 UTF-8 字符串。

示例(解码 Base64 URL)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.StringFromBase64Url)

console.log(decode("Zm9vYmFy"))
// Output: "foobar"

StringFromHex

把十六进制编码的字符串解码为 UTF-8 字符串。

示例(解码十六进制字符串)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.StringFromHex)

console.log(new TextEncoder().encode(decode("0001020304050607")))
/*
Output:
Uint8Array(8) [
  0, 1, 2, 3,
  4, 5, 6, 7
]
*/

StringFromUriComponent

把 URI 编码的字符串解码为 UTF-8 字符串。它适合在 URL 中编码与解码数据。

示例(解码 URI 组件)

import { Schema } from "effect"

const PaginationSchema = Schema.Struct({
  maxItemPerPage: Schema.Number,
  page: Schema.Number,
})

const UrlSchema = Schema.compose(
  Schema.StringFromUriComponent,
  Schema.parseJson(PaginationSchema),
)

console.log(Schema.encodeSync(UrlSchema)({ maxItemPerPage: 10, page: 1 }))
// Output: %7B%22maxItemPerPage%22%3A10%2C%22page%22%3A1%7D

数字转换

NumberFromString

使用 effect/Number 模块的 parse 函数解析字符串,从而把字符串变换为数字。

如果值无法转换(例如提供了非数字字符),它会返回错误。

支持以下特殊字符串值:“NaN”、“Infinity”、“-Infinity”。

示例(从字符串解析数字)

import { Schema } from "effect"

const schema = Schema.NumberFromString

const decode = Schema.decodeUnknownSync(schema)

// success cases
console.log(decode("1")) // 1
console.log(decode("-1")) // -1
console.log(decode("1.5")) // 1.5
console.log(decode("NaN")) // NaN
console.log(decode("Infinity")) // Infinity
console.log(decode("-Infinity")) // -Infinity

// failure cases
decode("a")
/*
throws:
ParseError: NumberFromString
└─ Transformation process failure
   └─ Expected NumberFromString, actual "a"
*/

clamp

把数字限制在指定范围内。

示例(限制数字范围)

import { Schema } from "effect"

// clamps the input to -1 <= x <= 1
const schema = Schema.Number.pipe(Schema.clamp(-1, 1))

const decode = Schema.decodeUnknownSync(schema)

console.log(decode(-3)) // -1
console.log(decode(0)) // 0
console.log(decode(3)) // 1

parseNumber

使用 effect/Number 模块的 parse 函数解析字符串,从而把字符串变换为数字。

如果值无法转换(例如提供了非数字字符),它会返回错误。

支持以下特殊字符串值:“NaN”、“Infinity”、“-Infinity”。

示例(解析并校验数字)

import { Schema } from "effect"

const schema = Schema.String.pipe(Schema.parseNumber)

const decode = Schema.decodeUnknownSync(schema)

console.log(decode("1")) // 1
console.log(decode("Infinity")) // Infinity
console.log(decode("NaN")) // NaN
console.log(decode("-"))
/*
throws
ParseError: (string <-> number)
└─ Transformation process failure
   └─ Expected (string <-> number), actual "-"
*/

布尔转换

Not

对布尔值取反。

示例(对布尔值取反)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.Not)

console.log(decode(true)) // false
console.log(decode(false)) // true

Symbol 转换

Symbol

使用 Symbol.for 把字符串转换为 symbol。

示例(从字符串创建 symbol)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.Symbol)

console.log(decode("a")) // Symbol(a)

BigInt 转换

BigInt

使用 BigInt 构造器把字符串转换为 BigInt

示例(从字符串解析 BigInt)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.BigInt)

// success cases
console.log(decode("1")) // 1n
console.log(decode("-1")) // -1n

// failure cases
decode("a")
/*
throws:
ParseError: bigint
└─ Transformation process failure
   └─ Expected bigint, actual "a"
*/
decode("1.5") // throws
decode("NaN") // throws
decode("Infinity") // throws
decode("-Infinity") // throws

BigIntFromNumber

使用 BigInt 构造器把数字转换为 BigInt

示例(从数字解析 BigInt)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.BigIntFromNumber)
const encode = Schema.encodeSync(Schema.BigIntFromNumber)

// success cases
console.log(decode(1)) // 1n
console.log(decode(-1)) // -1n
console.log(encode(1n)) // 1
console.log(encode(-1n)) // -1

// failure cases
decode(1.5)
/*
throws:
ParseError: BigintFromNumber
└─ Transformation process failure
   └─ Expected BigintFromNumber, actual 1.5
*/

decode(NaN) // throws
decode(Infinity) // throws
decode(-Infinity) // throws
encode(BigInt(Number.MAX_SAFE_INTEGER) + 1n) // throws
encode(BigInt(Number.MIN_SAFE_INTEGER) - 1n) // throws

clampBigInt

BigInt 限制在指定范围内。

示例(限制 BigInt 范围)

import { Schema } from "effect"

// clamps the input to -1n <= x <= 1n
const schema = Schema.BigIntFromSelf.pipe(Schema.clampBigInt(-1n, 1n))

const decode = Schema.decodeUnknownSync(schema)

console.log(decode(-3n))
// Output: -1n

console.log(decode(0n))
// Output: 0n

console.log(decode(3n))
// Output: 1n

Date 转换

Date

把字符串转换为合法的 Date,确保 new Date("Invalid Date") 这类非法日期会被拒绝。

示例(解析并校验日期)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.Date)

console.log(decode("1970-01-01T00:00:00.000Z"))
// Output: 1970-01-01T00:00:00.000Z

decode("a")
/*
throws:
ParseError: Date
└─ Predicate refinement failure
   └─ Expected Date, actual Invalid Date
*/

const validate = Schema.validateSync(Schema.Date)

console.log(validate(new Date(0)))
// Output: 1970-01-01T00:00:00.000Z

console.log(validate(new Date("Invalid Date")))
/*
throws:
ParseError: Date
└─ Predicate refinement failure
   └─ Expected Date, actual Invalid Date
*/

BigDecimal 转换

BigDecimal

把字符串转换为 BigDecimal

示例(从字符串解析 BigDecimal)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.BigDecimal)

console.log(decode(".124"))
// Output: { _id: 'BigDecimal', value: '124', scale: 3 }

BigDecimalFromNumber

把数字转换为 BigDecimal

Invalid Range

编码时,如果 BigDecimal 超出数字的 64 位范围, 这个 Schema 会产生不正确的结果。

示例(从数字解析 BigDecimal)

import { Schema } from "effect"

const decode = Schema.decodeUnknownSync(Schema.BigDecimalFromNumber)

console.log(decode(0.111))
// Output: { _id: 'BigDecimal', value: '111', scale: 3 }

clampBigDecimal

BigDecimal 限制在指定范围内。

示例(限制 BigDecimal 范围)

import { Schema } from "effect"
import { BigDecimal } from "effect"

const schema = Schema.BigDecimal.pipe(
  Schema.clampBigDecimal(BigDecimal.fromNumber(-1), BigDecimal.fromNumber(1)),
)

const decode = Schema.decodeUnknownSync(schema)

console.log(decode("-2"))
// Output: { _id: 'BigDecimal', value: '-1', scale: 0 }

console.log(decode("0"))
// Output: { _id: 'BigDecimal', value: '0', scale: 0 }

console.log(decode("3"))
// Output: { _id: 'BigDecimal', value: '1', scale: 0 }