Objects

属性

对象定义可以包含任何组合的必需、可选、可默认的命名属性和索引签名。

必需

const  = Symbol()

const  = ({
	requiredKey: "string",
	// Nested definitions don't require additional `type` calls!
	[]: {
		nested: "unknown"
	}
})

可选

const  = Symbol()

const  = ({
	"optionalKey?": "number[]",
	[]: "string?"
})

我应该为可选性使用键语法还是值语法?

可选性可以表示在键上或对应的值上。

我们推荐默认使用键语法,因为...

  • 它与 TypeScript 一致
  • 它更好地反映了 键存在 约束,对允许的值没有影响

然而,有几个原因你可能想使用值嵌入语法:

  1. 键是一个符号(使键嵌入语法不可能)
  2. 你想要编辑器功能,如 JSDoc 和转到定义对于键(如果键名更改,无法工作)
  3. 你真的讨厌必须引用键名

可选属性不能以 undefined 值存在

在 TypeScript 中,有一个名为 exactOptionalPropertyTypes 的设置,可以设置为 true 以强制区分缺失的属性和存在但值为 undefined 的属性。

ArkType 默认镜像此行为,因此如果你想允许 undefined,你需要将其添加到你的值定义中。虽然不是推荐的长期解决方案,你也可以全局配置 exactOptionalPropertyTypesfalse

查看示例
const  = ({
	"key?": "number"
})

// 有效数据
const  = MyObj({})

// 错误:键必须是数字(是 undefined)
const  = MyObj({ key: undefined })

可默认

const  = ({
	defaultableKey: "boolean = false"
})

可选和默认仅在对象和元组中有效!

与例如 number.array() 不同,number.optional()number.default(0) 不返回新的 Type,而是返回像 [Type<number>, "?"][Type<number>, "=", 0] 这样的元组定义。

这反映了这样一个事实:在 ArkType 的类型系统中,可选性和默认性仅相对于属性才有意义。尝试在对象外部创建可选或可默认的值,如 type("string?"),将导致 ParseError

要创建一个接受 stringundefinedType,使用像 type("string | undefined") 这样的联合。

要让它将 undefined 转换为一个空字符串,使用显式的 morph,如:

const  = ("string | undefined").pipe(v => v ?? "")

索引

const  = ({
	// 索引签名不需要标签
	"[string]": "number.integer",
	// 允许任意字符串或符号表达式
	"[string | symbol]": "number"
})

未声明

TypeScript 的结构类型系统明确允许分配带有额外键的对象,只要所有声明的约束都满足。ArkType 默认镜像此行为,因为通常...

  • 现有对象可以更频繁地重用。
  • 如果不需要检查未声明的键,验证会更高效。
  • 只要你声明的那些属性满足,额外的属性通常无关紧要。

然而,有时你使用对象的方式会使未声明的属性成为问题。尽管它们无法被 TypeScript 反映(yet - 请 +1 该问题!),ArkType 确实 支持拒绝或删除未声明的键。此行为可以使用下面的语法为单个对象定义,或通过配置 如果你想更改所有对象的默认行为。

// 如果存在除 "onlyAllowedKey" 以外的任何键则失败
const  = ({
	"+": "reject",
	onlyAllowedKey: "string"
})

// 删除除 "onlyPreservedStringKey" 以外的所有非符号键
const  = ({
	"+": "delete",
	"[symbol]": "unknown",
	onlyPreservedStringKey: "string"
})

// 允许并保留未声明的键(默认行为)
const  = ({
	// 仅在其他地方显式配置默认时指定 "ignore"
	"+": "ignore",
	nonexclusiveKey: "number"
})

展开

展开运算符 非常适合合并属性集。当应用于两个不同的(即不重叠的)属性集时,它等同于交集。然而,如果一个键同时出现在基础对象和合并对象中,基础值将被丢弃,转而使用合并的值,而不是递归交集。

展开绕过了交集的许多行为复杂性和计算开销,并且应该是组合属性集的首选方法。

基础对象定义可以通过在对象字面量中指定 "..." 作为第一个键来展开。随后属性将被合并到基础对象中,就像 JS 中的 ... 运算符一样

const  = ({ isAdmin: "false", name: "string" })

// 悬停查看新合并的对象
const  = ({
	"...": ,
	// 在交集中,isAdmin 的非重叠值将导致 ParseError
	isAdmin: "true",
	permissions: "string[]"
})

展开运算符在语义上等同于泛型 Merge 关键字,除了标准关键字语法外,还可以通过 Type 上的专用方法实例化。

const  = type.module({
	base: {
		"foo?": "0",
		"bar?": "0"
	},
	merged: {
		bar: "1",
		"baz?": "1"
	},
	result: "Merge<base, merged>"
})

// 悬停查看推断的结果
type  = typeof .result.infer

keyof

与 TypeScript 一样,keyof 运算符从对象中提取键作为联合:

const  = ({
	originallyPurchased: "string.date",
	remainingWheels: "number"
})

const  = .keyof()

type  = typeof .infer

也与 TypeScript 一样,如果对象包含像 [string] 这样的索引签名以及命名属性,keyof 的联合将简化为 string

const  = ({
	"[string]": "unknown",
	verySpecialKey: "0 < number <= 3.14159",
	moderatelySpecialKey: "-9.51413 <= number < 0"
})

// 在与 `string` 索引签名的联合中,字符串字面量
// "verySpecialKey" 和 "moderatelySpecialKey" 是多余的,将被修剪
const  = .keyof()

// 键与基础 `string` Type 相同
console.log(.equals("string"))

ArkType 的 `keyof` 永远不会包括 `number`

尽管 TypeScript 的 keyof 运算符可以产生 number,但在运行时的 JavaScript 中不存在数字键的概念。这导致了令人困惑和不一致的行为。在 ArkType 中,keyof 总是返回 stringsymbol,符合 JavaScript 对象的构造。

了解我们为什么在此问题上与 TypeScript 不同

在 JavaScript 中,你可以使用数字字面量定义键,但构造的值没有办法表示数字键,因此它被强制转换为字符串。

const  = {
	4: true,
	5: true
}

const  = {
	"4": true,
	"5": true
}

// numberLiteralObj 和 stringLiteralObj 在此时无法区分
Object.keys() // ["4", "5"]
Object.keys() // ["4", "5"]

对于一个基于集合的类型系统要正确,代表相同底层值集合的任何两个类型必须共享单一表示。TypeScript 决定为相同底层键具有不同的数字和字符串表示导致了一些最令人困惑的推理陷阱:

type  = {
	[x: string]: unknown
}

// Thing2 显然与 Thing1 相同
type  = <string, unknown>

// 然而...
type 
type Key1 = string | number
Key1
= keyof
type
type Key2 = string
Key2
= keyof

这种不一致性对于必须调和相同底层值集合的多个表示的类型系统是不可避免的。因此,数字键是 ArkType 故意与 TypeScript 不同的少数情况之一。ArkType 永远不会从 keyof 返回 number。键将始终被归一化为 stringsymbol,这是可以唯一附加到 JavaScript 对象的两种不同的属性类型。

get

与 TypeScript 中的索引访问表达式(例如 User["name"])一样,get 运算符基于指定的键定义从对象中提取值的 Type:

const  = type.enumerated("eating plants", "looking adorable")

const  = ({
	isFriendly: "true",
	snorf: {
		uses: .array()
	}
})

const  = .get("isFriendly")

// 通过传递额外参数可以直接访问嵌套属性
const  = .get("snorf", "uses")

像 `get` 和 `omit` 这样的表达式提取现有 Type 的部分可能是一种反模式!

在使用 get 提取你定义的属性的类型之前,考虑 是否可以直接将属性值定义为一个独立的可以轻松引用和组合的 Type。

通常,从底层组合 Type 比试图从现有 Type 中撕下你需要的部分更清晰和高效。

尽管像这样的情况相当直接,但当访问一个可能是一个联合、字面量或索引签名的任意键时,有许多更细微的行为需要考虑,该对象 Type 也可能是一个包含可选键或索引签名的联合。

如果你对这个(或 ArkType 中的任何其他内容)感兴趣深入了解,我们的单元测试 是我们拥有的最接近全面规范的东西。

不喜欢?没关系 - 你在编辑器中看到的推断类型和错误将始终引导你走向正确的方向 🧭

计划支持 TypeScript 的索引访问语法!

该 问题 上留言,让我们知道你是否 有兴趣使用 - 甚至帮助实现 - 类型级解析对于 字符串嵌入索引访问 🤓

数组

const  = ({
	key: "string[]"
})

长度

使用包含或不包含的最小或最大长度约束数组。

const  = ({
	nonEmptyStringArray: "string[] > 0",
	atLeast3Integers: "number.integer[] >= 3",
	lessThan10Emails: "string.email[] < 10",
	atMost5Booleans: "boolean[] <= 5"
})

范围表达式允许你指定最小和最大长度,并使用相同的语法表示排他性。

const  = ({
	nonEmptyStringArrayAtMostLength10: "0 < string[] <= 10",
	twoToFiveIntegers: "2 <= number.integer[] < 6"
})

元组

与对象一样,元组是其值是嵌套定义的结构。与 TypeScript 一样,ArkType 支持前缀、可选、可变和后缀元素,并对它们的组合有相同的限制。

前缀

const  = ([
	"string",
	// 对象定义可以嵌套在元组中 - 反之亦然!
	{
		coordinates: ["number", "number"]
	}
])

可默认

可默认元素是可选元素,如果在元组输入中不存在,将被分配其指定的默认值。

元组可以在其前缀元素之后和非可默认可选元素之前包含零个或多个可默认元素。

与可选元素一样,可默认元素与后缀元素互斥。

const  = (["string", "boolean = false", "number = 0"])

可选

可选元素是元组元素,可能在输入中存在或不存在,但没有默认值。

元组可以在其前缀和可默认元素之后以及可变元素或元组结束之前包含零个或多个可选元素。

与 TypeScript 一样,可选元素与后缀元素互斥。

const  = (["string", "bigint = 999n", "boolean?", "number?"])

可变

与 TypeScript 一样,可变元素允许零个或多个给定类型的连续值,并且在元组中最多出现一次。

它们使用前置 "..." 运算符指定一个数组元素。

// 允许一个字符串后跟零个或多个数字
const  = (["string", "...", "number[]"])

后缀

后缀元素是可变元素之后的必需元素。

它们与可选元素互斥。

// 允许零个或多个数字后跟一个布尔值,然后一个字符串
const  = (["...", "number[]", "boolean", "string"])

日期

字面量

日期字面量表示具有确切值的 Date 实例。

它们主要用于范围中。

const  = ({
	singleQuoted: "d'01-01-1970'",
	doubleQuoted: 'd"01-01-1970"'
})

范围

使用包含或不包含的最小或最大值约束 Date。

边界可以表示为对应的 Unix epoch 值或日期字面量数字

const  = ({
	dateInThePast: `Date < ${Date.now()}`,
	dateAfter2000: "Date > d'2000-01-01'",
	dateAtOrAfter1970: "Date >= 0"
})

范围表达式允许你指定最小和最大值,并使用相同的语法表示排他性。

const  = new Date()
	.setFullYear(new Date().getFullYear() - 10)
	.valueOf()

const  = ({
	dateInTheLast10Years: `${} <= Date < ${Date.now()}`
})

instanceof

大多数内置实例类型如 ArrayDate 可以直接作为关键字使用,但 instanceof 可以用于将类型约束到你自己的类之一。

class MyClass {}

const  = type.instanceOf(MyClass)

关键字

可以在这里找到 instanceof 关键字列表[/docs/keywords#instanceof] 与 ArrayFormData 的基础和子类型关键字一起。

On this page