Skip to content

Commit bad4e42

Browse files
authored
add exactOptionalPropertyTypes config setting (#1391)
1 parent 027dffb commit bad4e42

File tree

15 files changed

+179
-10
lines changed

15 files changed

+179
-10
lines changed

ark/attest/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ark/attest",
3-
"version": "0.45.1",
3+
"version": "0.45.2",
44
"license": "MIT",
55
"author": {
66
"name": "David Blass",

ark/docs/components/dts/util.ts

+1-1
Large diffs are not rendered by default.

ark/docs/content/docs/configuration/index.mdx

+66
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,72 @@ const out = userForm({
367367
})
368368
```
369369

370+
### exactOptionalPropertyTypes
371+
372+
By default, ArkType validates optional keys as if [TypeScript's `exactOptionalPropertyTypes` is set to `true`](https://www.typescriptlang.org/tsconfig/#exactOptionalPropertyTypes).
373+
374+
<details>
375+
<summary>See an example</summary>
376+
377+
```ts
378+
const myObj = type({
379+
"key?": "number"
380+
})
381+
382+
// valid data
383+
const validResult = myObj({})
384+
385+
// Error: key must be a number (was undefined)
386+
const errorResult = myObj({ key: undefined })
387+
```
388+
389+
</details>
390+
391+
This approach allows the most granular control over optionality, as `| undefined` can be added to properties that should accept it.
392+
393+
However, if you have not enabled TypeScript's `exactOptionalPropertyTypes` setting, you may globally configure ArkType's `exactOptionalPropertyTypes` to `false` to match TypeScript's behavior. If you do this, we'd recommend making a plan to enable `exactOptionalPropertyTypes` in the future.
394+
395+
```ts title="config.ts"
396+
import { configure } from "arktype/config"
397+
398+
// since the default in ArkType is `true`, this will only have an effect if set to `false`
399+
configure({ exactOptionalPropertyTypes: false })
400+
```
401+
402+
```ts title="app.ts"
403+
import "./config.ts"
404+
// import your config file before arktype
405+
import { type } from "arktype"
406+
407+
const myObj = type({
408+
"key?": "number"
409+
})
410+
411+
// valid data
412+
const validResult = myObj({})
413+
414+
// now also valid data (would be an error by default)
415+
const secondResult = myObj({ key: undefined })
416+
```
417+
418+
<Callout type="warn" title="exactOptionalPropertyTypes does not yet affect default values!">
419+
420+
```ts
421+
const myObj = type({
422+
key: "number = 5"
423+
})
424+
425+
// { key: 5 }
426+
const omittedResult = myObj({})
427+
428+
// { key: undefined }
429+
const undefinedResult = myObj({ key: undefined })
430+
```
431+
432+
Support for this is tracked as part of [this broader configurable defaultability issue](https://github.com/arktypeio/arktype/issues/1390).
433+
434+
</Callout>
435+
370436
### jitless
371437

372438
By default, when a `Type` is instantiated, ArkType will precompile optimized validation logic that will run when the type is invoked. This behavior is disabled by default in environments that don't support `new Function`, e.g. Cloudflare Workers.

ark/docs/content/docs/objects/index.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ const myObject = type({
9191

9292
In TypeScript, there is a setting called [`exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes) that can be set to `true` to enforce the distinction between properties that are missing and properties that are present with the value `undefined`.
9393

94-
ArkType mirrors this behavior by default, so if you want to allow `undefined`, you'll need to add it to your value's definition. If you're interested in a builtin configuration option for this setting, we'd love feedback or contributions on [this issue](https://github.com/arktypeio/arktype/issues/1191).
94+
ArkType mirrors this behavior by default, so if you want to allow `undefined`, you'll need to add it to your value's definition. Though not recommended as a long-term solution, you may also [globally configure `exactOptionalPropertyTypes`](/docs/configuration#exactoptionalpropertytypes) to `false`.
9595

9696
<details>
9797
<summary>See an example</summary>

ark/fs/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ark/fs",
3-
"version": "0.45.1",
3+
"version": "0.45.2",
44
"license": "MIT",
55
"author": {
66
"name": "David Blass",

ark/schema/config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export interface ArkSchemaConfig extends Partial<Readonly<NodeConfigsByKind>> {
117117
readonly onUndeclaredKey?: UndeclaredKeyBehavior
118118
readonly numberAllowsNaN?: boolean
119119
readonly dateAllowsInvalid?: boolean
120+
readonly exactOptionalPropertyTypes?: boolean
120121
readonly onFail?: ArkErrors.Handler | null
121122
readonly keywords?: Record<string, TypeMeta.Collapsible | undefined>
122123
}

ark/schema/kinds.ts

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ $ark.defaultConfig = withAlphabetizedKeys(
9191
jitless: envHasCsp(),
9292
clone: deepClone,
9393
onUndeclaredKey: "ignore",
94+
exactOptionalPropertyTypes: true,
9495
numberAllowsNaN: false,
9596
dateAllowsInvalid: false,
9697
onFail: null,

ark/schema/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ark/schema",
3-
"version": "0.45.1",
3+
"version": "0.45.2",
44
"license": "MIT",
55
"author": {
66
"name": "David Blass",

ark/schema/structure/optional.ts

+12
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
throwParseError,
66
type requireKeys
77
} from "@ark/util"
8+
import { intrinsic } from "../intrinsic.ts"
89
import type { Morph } from "../roots/morph.ts"
910
import type { BaseRoot } from "../roots/root.ts"
1011
import { compileSerializedValue } from "../shared/compile.ts"
@@ -62,6 +63,17 @@ const implementation: nodeImplementationOf<Optional.Declaration> =
6263
}
6364
},
6465
normalize: schema => schema,
66+
reduce: (inner, $) => {
67+
if ($.resolvedConfig.exactOptionalPropertyTypes === false) {
68+
if (!inner.value.allows(undefined)) {
69+
return $.node(
70+
"optional",
71+
{ ...inner, value: inner.value.or(intrinsic.undefined) },
72+
{ prereduced: true }
73+
)
74+
}
75+
}
76+
},
6577
defaults: {
6678
description: node => `${node.compiledKey}?: ${node.value.description}`
6779
},

ark/type/CHANGELOG.md

+61
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,66 @@
11
# arktype
22

3+
## 2.1.12
4+
5+
### `exactOptionalPropertyTypes`
6+
7+
By default, ArkType validates optional keys as if [TypeScript's `exactOptionalPropertyTypes` is set to `true`](https://www.typescriptlang.org/tsconfig/#exactOptionalPropertyTypes).
8+
9+
```ts
10+
const myObj = type({
11+
"key?": "number"
12+
})
13+
14+
// valid data
15+
const validResult = myObj({})
16+
17+
// Error: key must be a number (was undefined)
18+
const errorResult = myObj({ key: undefined })
19+
```
20+
21+
This approach allows the most granular control over optionality, as `| undefined` can be added to properties that should accept it.
22+
23+
However, if you have not enabled TypeScript's `exactOptionalPropertyTypes` setting, you may globally configure ArkType's `exactOptionalPropertyTypes` to `false` to match TypeScript's behavior. If you do this, we'd recommend making a plan to enable `exactOptionalPropertyTypes` in the future.
24+
25+
```ts title="config.ts"
26+
import { configure } from "arktype/config"
27+
28+
// since the default in ArkType is `true`, this will only have an effect if set to `false`
29+
configure({ exactOptionalPropertyTypes: false })
30+
```
31+
32+
```ts title="app.ts"
33+
import "./config.ts"
34+
// import your config file before arktype
35+
import { type } from "arktype"
36+
37+
const myObj = type({
38+
"key?": "number"
39+
})
40+
41+
// valid data
42+
const validResult = myObj({})
43+
44+
// now also valid data (would be an error by default)
45+
const secondResult = myObj({ key: undefined })
46+
```
47+
48+
**WARNING: exactOptionalPropertyTypes does not yet affect default values!**
49+
50+
```ts
51+
const myObj = type({
52+
key: "number = 5"
53+
})
54+
55+
// { key: 5 }
56+
const omittedResult = myObj({})
57+
58+
// { key: undefined }
59+
const undefinedResult = myObj({ key: undefined })
60+
```
61+
62+
Support for this is tracked as part of [this broader configurable defaultability issue](https://github.com/arktypeio/arktype/issues/1390).
63+
364
## 2.1.11
465

566
- Expose `select` method directly on `Type` (previously was only available on `.internal`)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { configure } from "arktype/config"
2+
3+
export const config = configure({
4+
exactOptionalPropertyTypes: false
5+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import "./eoptConfig.ts"
2+
3+
import { type } from "arktype"
4+
import { equal } from "node:assert/strict"
5+
import { cases } from "./util.ts"
6+
7+
cases({
8+
fromDef: () => {
9+
const o = type({
10+
"name?": "string"
11+
})
12+
13+
equal(o.expression, "{ name?: string | undefined }")
14+
},
15+
fromRef: () => {
16+
const o = type({
17+
"name?": type.string
18+
})
19+
20+
equal(o.expression, "{ name?: string | undefined }")
21+
}
22+
})

ark/type/package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "arktype",
33
"description": "TypeScript's 1:1 validator, optimized from editor to runtime",
4-
"version": "2.1.11",
4+
"version": "2.1.12",
55
"license": "MIT",
66
"repository": {
77
"type": "git",
@@ -36,10 +36,11 @@
3636
"scripts": {
3737
"build": "ts ../repo/build.ts",
3838
"test": "ts ../repo/testPackage.ts; pnpm testIntegration",
39-
"testIntegration": "pnpm testSimpleConfig && pnpm testAllConfig && pnpm testOnFailConfig",
39+
"testIntegration": "pnpm testSimpleConfig && pnpm testAllConfig && pnpm testOnFailConfig && pnpm testEoptConfig",
4040
"testSimpleConfig": "ts ./__tests__/integration/testSimpleConfig.ts",
4141
"testAllConfig": "ts ./__tests__/integration/testAllConfig.ts",
42-
"testOnFailConfig": "ts ./__tests__/integration/testOnFailConfig.ts"
42+
"testOnFailConfig": "ts ./__tests__/integration/testOnFailConfig.ts",
43+
"testEoptConfig": "ts ./__tests__/integration/testEoptConfig.ts"
4344
},
4445
"dependencies": {
4546
"@ark/util": "workspace:*",

ark/util/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ark/util",
3-
"version": "0.45.1",
3+
"version": "0.45.2",
44
"license": "MIT",
55
"author": {
66
"name": "David Blass",

ark/util/registry.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { FileConstructor, objectKindOf } from "./objectKinds.ts"
88
// recent node versions (https://nodejs.org/api/esm.html#json-modules).
99

1010
// For now, we assert this matches the package.json version via a unit test.
11-
export const arkUtilVersion = "0.45.1"
11+
export const arkUtilVersion = "0.45.2"
1212

1313
export const initialRegistryContents = {
1414
version: arkUtilVersion,

0 commit comments

Comments
 (0)