Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion ark/docs/content/docs/ecosystem/meta.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"title": "Ecosystem",
"icon": "New",
"pages": ["[ArkEnv](/docs/ecosystem#arkenv)", "[DRZL](/docs/ecosystem#drzl)", "[shorn](/docs/ecosystem#shorn)"]
"pages": [
"[ArkEnv](/docs/ecosystem#arkenv)",
"[DRZL](/docs/ecosystem#drzl)",
"[shorn](/docs/ecosystem#shorn)"
]
}
62 changes: 62 additions & 0 deletions ark/docs/content/docs/objects/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ However, if a key appears in both the base and merged objects, the base value wi

Spreading bypasses a lot of the behavioral complexity and computational overhead of an intersection and should be the preferred method of combining property sets.

Like ArkType's other [structural transformations](/docs/objects#properties-structural), `merge` returns a new object built from the properties it extracts, so a root narrow or metadata is not carried over.

<SyntaxTabs>
<SyntaxTab fluent>

Expand Down Expand Up @@ -562,6 +564,42 @@ Not your cup of tea? No worries- the inferred types and errors you'll see in edi
string-embedded index access 🤓
</Callout>

### structural transformations [#properties-structural]

`merge`, `pick`, `omit`, `required`, `partial` and `map` each return a **new object built from the properties** of the `Type` they're applied to rather than referencing it.

<Callout type="warn" title="Only properties are carried over">

Anything attached to the object itself is not part of that result- most importantly a root [narrow](/docs/expressions#narrow) or [filter](/docs/expressions#filter), but also [metadata](/docs/configuration#metadata) like `.describe(...)` and constraints like `.atLeastLength(...)`. Constraints on individual properties are unaffected.

```ts
const User = type({
name: "string",
email: "string"
}).narrow(user => user.email.includes("@"))

// Type<{ name?: string; email?: string }>- the narrow is not a property
const PartialUser = User.partial()

// no error, though User itself would have rejected this
const out = PartialUser({ email: "not an email" })
```

Reapply anything you still need to the result:

```ts
const PartialUser = type({
name: "string",
email: "string"
})
.partial()
.narrow(user => user.email?.includes("@") ?? true)
```

A root [morph](/docs/expressions#pipe) is the exception. Since it can't be reduced to a set of properties, transforming a `Type` like `type({ name: "string" }).pipe(user => user)` throws a `ParseError` rather than discarding it.

</Callout>

### pick / omit [#properties-pick-omit]

Extract or exclude specific properties from an object Type:
Expand All @@ -582,6 +620,8 @@ const WithoutEmail = User.omit("email")

These are also available as [generic keywords](/docs/generics): `Pick(User, "name | email")`, `Omit(User, "email")`.

Like ArkType's other [structural transformations](/docs/objects#properties-structural), `pick` and `omit` return a new object built from the properties they extract, so a root narrow or metadata is not carried over.

### required / partial [#properties-required-partial]

Make all named properties required or optional:
Expand All @@ -607,6 +647,26 @@ const PartialConfig = Config.partial()

These are also available as [generic keywords](/docs/generics): `Required(User)`, `Partial(Config)`.

Like ArkType's other [structural transformations](/docs/objects#properties-structural), `required` and `partial` return a new object built from the properties they extract, so a root narrow or metadata is not carried over.

<Callout type="info" title="Arrays and tuples mirror TS's `Required` and `Partial`">

Like the homomorphic mapped types they're named for, `required` and `partial` preserve an array or tuple base and apply to its elements:

```ts
// Type<[string?, number?]>
const PartialPair = type(["string", "number"]).partial()

// Type<[string, number]>
const RequiredPair = type(["string", "number?"]).required()
```

They only ever change whether an element must be **present**, never the values it allows. TS additionally unions variadic elements and index signature values with `undefined`, since it has no modifier to mark either optional. ArkType doesn't, so `type("string[]").partial()` is still `string[]`.

A postfix element can't be optional- `[string?, ...number[], boolean]` is as unrepresentable in ArkType as it is in TS. `partial` therefore throws a `ParseError` on a tuple like `[string, ...number[], boolean]` rather than widening every element the way TS's `Partial` does.

</Callout>

### readonly [#properties-readonly]

Mark all properties as readonly (type-level only, no runtime effect):
Expand All @@ -624,6 +684,8 @@ const Frozen = type({

Transform the properties of an object Type using a mapping function. The mapper receives a prop entry with `key`, `value`, and `kind` (`"required"` or `"optional"`). Return a `{ key, value }` object (optionally with `kind`) to transform, or an empty array `[]` to remove the property.

Like ArkType's other [structural transformations](/docs/objects#properties-structural), `map` returns a new object built from the properties it extracts, so a root narrow or metadata is not carried over.

```ts
// @noErrors
const User = type({
Expand Down
1 change: 1 addition & 0 deletions ark/docs/content/docs/objects/properties/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"[spread](/docs/objects#properties-spread)",
"[keyof](/docs/objects#properties-keyof)",
"[get](/docs/objects#properties-get)",
"[structural transformations](/docs/objects#properties-structural)",
"[pick / omit](/docs/objects#properties-pick-omit)",
"[required / partial](/docs/objects#properties-required-partial)",
"[readonly](/docs/objects#properties-readonly)",
Expand Down
112 changes: 107 additions & 5 deletions ark/docs/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -549,10 +549,7 @@ These selectors can also be used to [target specific references for configuratio
const User = type({ name: "string", age: "number" })

// add the description to all domain nodes
const configured = User.configure(
{ description: "a special string" },
"domain"
)
const configured = User.configure({ description: "a special string" }, "domain")

configured.get("name").description // "a special string"
configured.get("age").description // "a special string"
Expand Down Expand Up @@ -1596,6 +1593,49 @@ console.log(env.PORT) // (property) PORT: number
console.log(env.NODE_ENV) // (property) NODE_ENV: "development" | "production" | "test"
```

### DRZL

[DRZL](https://use-drzl.github.io/drzl) is zero-friction codegen for Drizzle ORM, tailored for ArkType developers. It analyzes your Drizzle schemas and generates ArkType validation schemas, services, and routers—eliminating boilerplate and ensuring seamless type safety between your database and application layers.

```ts
// @noErrors
// drzl.config.ts
import { defineConfig } from "@drzl/cli/config"

export default defineConfig({
schema: "src/db/schemas/index.ts",
outDir: "src/api",
generators: [
// 1) ArkType validators
{ kind: "arktype", path: "src/validators/arktype", schemaSuffix: "Schema" },

// 2) Routers (oRPC adapter), reusing ArkType schemas
{
kind: "orpc",
template: "@drzl/template-orpc-service",
includeRelations: true,
outputHeader: { enabled: true },
validation: {
useShared: true,
library: "arktype",
importPath: "src/validators/arktype",
schemaSuffix: "Schema"
}
},
// 3) Typed services (Drizzle-aware or stub)
{
kind: "service",
path: "src/services",
dataAccess: "drizzle", // or 'stub'
dbImportPath: "src/db/connection",
schemaImportPath: "src/db/schemas"
}
]
})
```

> For more details, see the [Getting Started guide](https://use-drzl.github.io/drzl/guide/getting-started.html).


---
title: Expressions
Expand Down Expand Up @@ -4272,6 +4312,8 @@ However, if a key appears in both the base and merged objects, the base value wi

Spreading bypasses a lot of the behavioral complexity and computational overhead of an intersection and should be the preferred method of combining property sets.

Like ArkType's other [structural transformations](/docs/objects#properties-structural), `merge` returns a new object built from the properties it extracts, so a root narrow or metadata is not carried over.

<SyntaxTabs>
<SyntaxTab fluent>

Expand Down Expand Up @@ -4542,6 +4584,42 @@ Not your cup of tea? No worries- the inferred types and errors you'll see in edi
string-embedded index access 🤓
</Callout>

### structural transformations [#properties-structural]

`merge`, `pick`, `omit`, `required`, `partial` and `map` each return a **new object built from the properties** of the `Type` they're applied to rather than referencing it.

<Callout type="warn" title="Only properties are carried over">

Anything attached to the object itself is not part of that result- most importantly a root [narrow](/docs/expressions#narrow) or [filter](/docs/expressions#filter), but also [metadata](/docs/configuration#metadata) like `.describe(...)` and constraints like `.atLeastLength(...)`. Constraints on individual properties are unaffected.

```ts
const User = type({
name: "string",
email: "string"
}).narrow(user => user.email.includes("@"))

// Type<{ name?: string; email?: string }>- the narrow is not a property
const PartialUser = User.partial()

// no error, though User itself would have rejected this
const out = PartialUser({ email: "not an email" })
```

Reapply anything you still need to the result:

```ts
const PartialUser = type({
name: "string",
email: "string"
})
.partial()
.narrow(user => user.email?.includes("@") ?? true)
```

A root [morph](/docs/expressions#pipe) is the exception. Since it can't be reduced to a set of properties, transforming a `Type` like `type({ name: "string" }).pipe(user => user)` throws a `ParseError` rather than discarding it.

</Callout>

### pick / omit [#properties-pick-omit]

Extract or exclude specific properties from an object Type:
Expand All @@ -4562,6 +4640,8 @@ const WithoutEmail = User.omit("email")

These are also available as [generic keywords](/docs/generics): `Pick(User, "name | email")`, `Omit(User, "email")`.

Like ArkType's other [structural transformations](/docs/objects#properties-structural), `pick` and `omit` return a new object built from the properties they extract, so a root narrow or metadata is not carried over.

### required / partial [#properties-required-partial]

Make all named properties required or optional:
Expand All @@ -4587,6 +4667,26 @@ const PartialConfig = Config.partial()

These are also available as [generic keywords](/docs/generics): `Required(User)`, `Partial(Config)`.

Like ArkType's other [structural transformations](/docs/objects#properties-structural), `required` and `partial` return a new object built from the properties they extract, so a root narrow or metadata is not carried over.

<Callout type="info" title="Arrays and tuples mirror TS's `Required` and `Partial`">

Like the homomorphic mapped types they're named for, `required` and `partial` preserve an array or tuple base and apply to its elements:

```ts
// Type<[string?, number?]>
const PartialPair = type(["string", "number"]).partial()

// Type<[string, number]>
const RequiredPair = type(["string", "number?"]).required()
```

They only ever change whether an element must be **present**, never the values it allows. TS additionally unions variadic elements and index signature values with `undefined`, since it has no modifier to mark either optional. ArkType doesn't, so `type("string[]").partial()` is still `string[]`.

A postfix element can't be optional- `[string?, ...number[], boolean]` is as unrepresentable in ArkType as it is in TS. `partial` therefore throws a `ParseError` on a tuple like `[string, ...number[], boolean]` rather than widening every element the way TS's `Partial` does.

</Callout>

### readonly [#properties-readonly]

Mark all properties as readonly (type-level only, no runtime effect):
Expand All @@ -4604,6 +4704,8 @@ const Frozen = type({

Transform the properties of an object Type using a mapping function. The mapper receives a prop entry with `key`, `value`, and `kind` (`"required"` or `"optional"`). Return a `{ key, value }` object (optionally with `kind`) to transform, or an empty array `[]` to remove the property.

Like ArkType's other [structural transformations](/docs/objects#properties-structural), `map` returns a new object built from the properties it extracts, so a root narrow or metadata is not carried over.

```ts
// @noErrors
const User = type({
Expand All @@ -4626,7 +4728,7 @@ The `props` getter returns an array of property descriptors for introspection:
const User = type({
name: "string",
"age?": "number",
"role": "'admin' | 'user' = 'user'"
role: "'admin' | 'user' = 'user'"
})

for (const prop of User.props) {
Expand Down
19 changes: 17 additions & 2 deletions ark/schema/roots/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,24 @@ export abstract class BaseRoot<
: operation === "partial" ? "optionalize"
: operation

const transformed = structure[structuralMethodName](
...(args as [never])
) as Structure.Node

// like the mapped types they're named for, `required` and `partial` are
// homomorphic, preserving an array or tuple base
const isHomomorphic = operation === "required" || operation === "partial"

// they are also the only operations that can be a no-op (e.g. on an
// array with no fixed elements), in which case the original branch is
// preserved rather than reduced to a new object
if (isHomomorphic && transformed.equals(structure)) return branch

return this.$.node("intersection", {
domain: "object",
structure: structure[structuralMethodName](...(args as [never]))
...(isHomomorphic && transformed.sequence ?
{ proto: Array }
: { domain: "object" }),
structure: transformed
})
})
}
Expand Down
21 changes: 21 additions & 0 deletions ark/schema/structure/sequence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,27 @@ export class SequenceNode extends BaseConstraint<Sequence.Declaration> {
registeredReference(this.defaultValueMorphs)
: undefined

optionalize(): SequenceNode {
const { prefix, defaultables, ...inner } = this.inner
// without a prefix, every element is already optional. bailing here
// preserves defaultables, which would otherwise have to be flattened
// into optionals to maintain their position relative to the prefix.
if (!prefix) return this

return this.$.node("sequence", {
...inner,
optionals: conflatenate(prefix, this.defaultablesAndOptionals)
})
}

require(): SequenceNode {
const { defaultables, optionals, ...inner } = this.inner
return this.$.node("sequence", {
...inner,
prefix: conflatenate(this.prefix, this.defaultablesAndOptionals)
})
}

protected elementAtIndex(data: array, index: number): SequenceElement {
if (index < this.prevariadic.length) return this.tuple[index]
const firstPostfixIndex = data.length - this.postfixLength
Expand Down
2 changes: 2 additions & 0 deletions ark/schema/structure/structure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ export class StructureNode extends BaseConstraint<Structure.Declaration> {
const { required, ...inner } = this.inner
return this.$.node("structure", {
...inner,
...(inner.sequence ? { sequence: inner.sequence.optionalize() } : {}),
optional: this.props.map(prop =>
prop.hasKind("required") ? this.$.node("optional", prop.inner) : prop
)
Expand All @@ -577,6 +578,7 @@ export class StructureNode extends BaseConstraint<Structure.Declaration> {
const { optional, ...inner } = this.inner
return this.$.node("structure", {
...inner,
...(inner.sequence ? { sequence: inner.sequence.require() } : {}),
required: this.props.map(prop =>
prop.hasKind("optional") ?
{
Expand Down
40 changes: 40 additions & 0 deletions ark/type/__tests__/keywords/partial.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,44 @@ contextualize(() => {

attest(T.expression).snap("{ [string]: number, bar?: 1, foo?: 1 }")
})

it("tuple", () => {
const T = type(["string", "number"]).partial()

attest<[string?, number?]>(T.t)
attest(T.expression).snap("[string?, number?]")
attest(T([])).equals([])
attest(T(["foo"])).equals(["foo"])
attest(T({}).toString()).snap("must be an array (was object)")
})

it("array is unaffected", () => {
// like the index signature above, TS unions a variadic element with
// undefined since it has no way to represent an optional one. in
// ArkType, optionality is about presence rather than the values an
// element allows, so the type is unchanged.
const T = type("string[]").partial()

attest<(string | undefined)[]>(T.t)
attest(T.expression).snap("string[]")
attest(T([undefined]).toString()).snap(
"value at [0] must be a string (was undefined)"
)
})

it("preserves defaultable elements", () => {
const T = type(["number = 5"]).partial()

attest(T.expression).snap("[number = 5]")
})

it("postfix element", () => {
// TS folds postfix elements into the variadic here, which would allow
// values the original tuple never did
attest(() =>
type(["string", "...", "number[]", "boolean"]).partial()
).throws.snap(
"ParseError: A postfix required element cannot follow an optional or defaultable element"
)
})
})
Loading
Loading