宣布 ArkType 2.1

从类型语法优化的模式匹配

从今天起,2.1.0 正式可用 🎉

最大的特性是 match,这是一个模式匹配 API,它允许您使用富有表现力的类型语法定义案例。结果是一个高度优化的匹配器,它使用集合理论自动跳过不匹配的分支。

我们非常兴奋地分享这个特性,不仅因为它是 JS 中第一个语法匹配器,而且因为它是 ArkType 第一个展示运行时类型潜力(不仅仅是验证)的特性。

具有可内省类型的语言提供了令人难以置信的强大特性,这些特性在 JS 中一直感觉遥不可及——直到现在。

const  = ({
	"string | number | boolean | null": v => v,
	bigint: b => `${b}n`,
	object: o => {
		for (const  in o) {
			o[] = (o[])
		}
		return o
	},
	default: "assert"
})

const  = (value: unknown) =>
	tsPatternMatch(value)
		.with(P.union(P.string, P.number, P.boolean, null), v => v)
		.with(P.bigint, v => `${v}n`)
		.with({}, o => {
			for (const  in o) {
				o[] = (o[])
			}
			return o
		})
		.otherwise(() => {
			throw new Error("value is not valid JSON")
		})

// "foo" (9 纳秒)
("foo")
// "foo" (765 纳秒)
("foo")

// "5n" (33 纳秒)
(5n)
// "5n" (924 纳秒)
(5n)

// { nestedValue: "5n" } (44 纳秒)
({ nestedValue: 5n })
// { nestedValue: "5n" } (2080 纳秒)
({ nestedValue: 5n })

我们实际上是 Gabriel Vergnaudts-pattern 的忠实粉丝,它有一个很棒的 API 和完全合理的性能。我们参考它进行比较,以展示运行时类型解锁的独特表现力和优化潜力。

下面是 2.1.0 发布的完整说明。我们迫不及待想听听您的想法!🚀

match

match 函数提供了一种强大的方式来处理不同类型的输入,并根据输入类型返回相应的输出,就像一个类型安全的 switch 语句。

Case Record API

定义匹配器的最简单方式是使用 ArkType 定义字符串作为键,并使用相应的处理程序作为值:

import { match } from "arktype"

const  = match({
	"string | Array": v => v.length,
	number: v => v,
	bigint: v => v,
	default: "assert"
})

// 一旦指定了 `default`,匹配定义就完整了,
// 无论是作为案例还是通过 .default() 方法

("abc") // 3
([1, 2, 3, 4]) // 4
(5n) // 5n
// ArkErrors: 必须是对象、字符串、数字或 bigint(是 boolean)
(true)

在这个示例中,sizeOf 是一个匹配器,它接受字符串、数组、数字或 bigint 作为输入。它返回字符串和数组的长度,以及数字和 bigint 的值。

default 接受 4 种值之一:

  • "assert": 接受 unknown,如果没有案例匹配则抛出错误
  • "never": 基于推断的案例接受输入,如果没有匹配则抛出错误
  • "reject": 接受 unknown,如果没有案例匹配则返回 ArkErrors
  • (data: In) => unknown: 直接处理不匹配其他案例的数据

案例将按照指定的顺序进行检查,无论是作为对象字面量的键还是通过链式方法。

Fluent API

match 函数还提供了一个流畅 API。这对于非字符串可嵌入的定义很方便:

// Case Record 和 Fluent API 可以轻松结合
const  = ({
	string: v => v.length,
	number: v => v,
	bigint: v => v
})
	// 匹配任何具有数字 length 属性的对象并提取它
	.case({ length: "number" }, o => o.length)
	// 为所有其他数据返回 0
	.default(() => 0)

("abc") // 3
({ name: "David", length: 5 }) // 5
(null) // 0

使用 in 缩小输入范围,使用 at 进行属性匹配

type  =
	| {
			id: 1
			oneValue: number
	  }
	| {
			id: 2
			twoValue: string
	  }

const  = 
	// .in 允许您指定 TypeScript 允许的输入类型
	.in<>()
	// .at 允许您指定输入将匹配的键
	.at("id")
	.match({
		1: o => `${o.oneValue}!`,
		2: o => o.twoValue.length,
		default: "assert"
	})

({ id: 1, oneValue: 1 }) // "1!"
({ id: 2, twoValue: "two" }) // 3
({ oneValue: 3 })
TypeScript: Property 'id' is missing in type '{ oneValue: number; }' but required in type '{ id: 1; oneValue: number; }'.

特别感谢 @thetayloredman,他出色地帮助我们迭代当前类型级模式匹配实现🙇

内置关键字现在可以全局配置

这对于自定义错误消息非常有帮助,而无需创建自己的别名或包装器。

config.ts
import { configure } from "arktype/config"

configure({
	keywords: {
		string: "shorthand description",
		"string.email": {
			actual: () => "definitely fake"
		}
	}
})
app.ts
import "./config.ts"
import { type } from "arktype"

const  = type({
	name: "string",
	email: "string.email"
})

const  = User({
	// ArkErrors: name 必须是 shorthand description(是数字)
	name: 5,
	// ArkErrors: email 必须是电子邮件地址(是 definitely fake)
	email: "449 Canal St"
})

您在这里提供的选项与用于 直接配置 Type 的选项相同,还可以 在类型级扩展以包含自定义元数据

元组和 args 表达式用于 .to

如果 morph 返回 ArkErrors 实例,验证将以该结果失败,而不是将其视为值。这特别适用于将其他 Type 用作 morph 来验证输出或链式转换。

为了简化这一点,有一个特殊的 to 操作符,可以管道到解析定义,而无需将其包装在 type 中使其成为函数。

这在 2.0 之前就已添加,但现在它带有对应的操作符 (|>),因此可以通过元组或 args 与大多数其他表达式类似地表达:

const  = ("string.numeric.parse").to("number % 2")

const  = ({
	someKey: ["string.numeric.parse", "|>", "number % 2"]
})

const  = ("string.numeric.parse", "|>", "number % 2")

错误配置现在直接接受字符串

const  = ("1", "@", {
	// 以前这里只允许返回字符串的函数
	message: "Yikes."
})

// ArkErrors: Yikes.
CustomOne(2)

请记住,如文档所述,像 message 这样的错误配置可能会覆盖更细粒度的配置选项,如 expectedactual,并且不能包含在复合错误中,例如联合类型。

尽管通常基于上下文返回字符串是最佳选择,但在您总是想要相同静态消息的情况下,现在更容易实现!

Type.toString() 现在将语法表示包装在 Type<..>

以前,Type.toString() 只是返回 Type.expression。然而,在消息来源不总是 Type 的上下文中,这可能会令人困惑:

// < 2.1.0:  "(was string)"
// >= 2.1.0: "(was Type<string>)"
console.log(`(was ${type.string})`)

希望如果您插值一个 Type,从现在起结果会让您更少困惑!

改进 Type 实例在外部泛型中的推断方式

以前,我们在某些 Type 方法返回中使用 NoInfer。在迁移到内联条件后,我们获得了相同的益处,并且像这样的外部推断案例更可靠:

function fn<
	T extends {
		schema: 
	}
>(_: T) {
	return {} as StandardSchemaV1.<T["schema"]>
}

// 以前推断为 unknown(现在正确推断为 { name: string })
const  = fn({
	schema: ({
		name: "string"
	})
})

修复导致某些区分联合类型错误拒绝默认案例的问题

const  = ({
	id: "0",
	k1: "number"
})
	.or({ id: "1", k1: "number" })
	.or({
		name: "string"
	})

// 以前,这被拒绝为需要 "k1" 键
// 现在将命中 id: 1 的区分案例,
// 但仍通过 { name: string } 分支正确允许
Discriminated({ name: "foo", id: 1 })