Skip to content

Commit

Permalink
fix inference on constrained index signature (#986)
Browse files Browse the repository at this point in the history
  • Loading branch information
ssalbdivad authored May 29, 2024
1 parent d847190 commit fbcdddc
Show file tree
Hide file tree
Showing 9 changed files with 68 additions and 18 deletions.
3 changes: 3 additions & 0 deletions .changeset/quiet-hairs-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
"@arktype/schema": patch
---
4 changes: 2 additions & 2 deletions ark/schema/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export type Constraints = {
literal?: string | number
}

declare const constrained: unique symbol
export declare const constrained: unique symbol

type constrained = typeof constrained
export type constrained = typeof constrained

export type of<base, constraints extends Constraints> = [
base,
Expand Down
4 changes: 3 additions & 1 deletion ark/schema/keywords/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const creditCard = root.defineRoot({
export interface validationExports {
alpha: string
alphanumeric: string
digits: string
lowercase: string
uppercase: string
creditCard: string
Expand All @@ -72,7 +73,8 @@ export type validation = SchemaModule<validationExports>
export const validation: validation = schemaScope(
{
alpha: defineRegex(/^[A-Za-z]*$/, "only letters"),
alphanumeric: defineRegex(/^[A-Za-z\d]*$/, "only letters and digits"),
alphanumeric: defineRegex(/^[A-Za-z\d]*$/, "only letters and digits 0-9"),
digits: defineRegex(/^\d*$/, "only digits 0-9"),
lowercase: defineRegex(/^[a-z]*$/, "only lowercase letters"),
uppercase: defineRegex(/^[A-Z]*$/, "only uppercase letters"),
creditCard,
Expand Down
17 changes: 17 additions & 0 deletions ark/type/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# arktype

## 2.0.0-dev.18

Add a `"digits"` keyword for strings consisting exclusively of 0-9.

Fix an issue causing index signatures with constraints like regex to be considered invalid as definitions.

The following is valid and now will be allowed as a definition.

```ts
const test = scope({
svgPath: /^\.\/(\d|a|b|c|d|e|f)+(-(\d|a|b|c|d|e|f)+)*\.svg$/,
svgMap: {
"[svgPath]": "digits"
}
}).export()
```

## 2.0.0-dev.17

- Error thrown by `.assert` or `out.throw()` is now an instance of [AggregateError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError), with the cause being an `ArkErrors` array.
Expand Down
2 changes: 2 additions & 0 deletions ark/type/__tests__/expressions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ contextualize(
"url",
"alpha",
"alphanumeric",
"digits",
"lowercase",
"uppercase",
"creditCard",
Expand Down Expand Up @@ -94,6 +95,7 @@ contextualize(
"url",
"alpha",
"alphanumeric",
"digits",
"lowercase",
"uppercase",
"creditCard",
Expand Down
9 changes: 8 additions & 1 deletion ark/type/__tests__/keywords.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,14 @@ contextualize(
attest(alphanumeric("user")).snap("user")
attest(alphanumeric("123")).snap("123")
attest(alphanumeric("abc@123").toString()).equals(
'must be only letters and digits (was "abc@123")'
'must be only letters and digits 0-9 (was "abc@123")'
)
})
it("digits", () => {
const digits = type("digits")
attest(digits("123")).snap("123")
attest(digits("user123").toString()).equals(
'must be only digits 0-9 (was "user123")'
)
})
it("lowercase", () => {
Expand Down
33 changes: 23 additions & 10 deletions ark/type/__tests__/realWorld.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { attest, contextualize } from "@arktype/attest"
import type { AtLeastLength, AtMostLength, Out, string } from "@arktype/schema"
import { registeredReference } from "@arktype/util"
import { scope, type, type Type } from "arktype"
import type { Module } from "../module.js"

contextualize(() => {
// https://github.com/arktypeio/arktype/issues/915
Expand Down Expand Up @@ -430,16 +431,28 @@ nospace must be matched by ^\\S*$ (was "One space")`)

attest(out).snap([{ token: "lovelace", amount: "5000000n" }])
})
it("union with domain and proto", () => {
const t = type("RegExp | string")
attest(t.raw.assertHasKind("union").discriminantJson).snap({
kind: "domain",
path: [],
cases: { '"string"': true, '"object"': { proto: "RegExp" } }

it("regex index signature", () => {
const test = scope({
svgPath: /^\.\/(\d|a|b|c|d|e|f)+(-(\d|a|b|c|d|e|f)+)*\.svg$/,
svgMap: {
"[svgPath]": "digits"
}
}).export()
attest<
Module<{
svgMap: {
[x: string & string.matching<string>]: string
}
svgPath: string.matching<string>
}>
>(test)
attest(test.svgMap({ "./f.svg": "123", bar: 5 })).unknown.snap({
"./f.svg": "123",
bar: 5
})
attest(t.allows("es")).equals(true)
attest(t.allows(5)).equals(false)
attest(t("es")).equals("es")
attest(t(new Date()).toString()).snap("must be a RegExp (was Date)")
attest(test.svgMap({ "./f.svg": "123a" }).toString()).snap(
'value at ["./f.svg"] must be only digits 0-9 (was "123a")'
)
})
})
2 changes: 1 addition & 1 deletion ark/type/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "arktype",
"description": "TypeScript's 1:1 validator, optimized from editor to runtime",
"version": "2.0.0-dev.17",
"version": "2.0.0-dev.18",
"license": "MIT",
"author": {
"name": "David Blass",
Expand Down
12 changes: 9 additions & 3 deletions ark/type/parser/objectLiteral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type Default,
type MutableInner,
type NodeSchema,
type of,
type PropKind,
type StructureNode,
type UndeclaredKeyBehavior,
Expand All @@ -17,10 +18,10 @@ import {
stringAndSymbolicEntriesOf,
throwParseError,
unset,
type anyOrNever,
type Dict,
type ErrorMessage,
type Key,
type anyOrNever,
type keyError,
type merge,
type mutable,
Expand Down Expand Up @@ -150,7 +151,9 @@ export type validateObjectLiteral<def, $, args> = {
validateString<indexDef, $, args> extends ErrorMessage<infer message> ?
// add a nominal type here to avoid allowing the error message as input
keyError<message>
: inferDefinition<indexDef, $, args> extends PropertyKey ?
: inferDefinition<indexDef, $, args> extends (
PropertyKey | of<PropertyKey, {}>
) ?
// if the indexDef is syntactically and semantically valid,
// move on to the validating the value definition
validateDefinition<def[k], $, args>
Expand All @@ -177,7 +180,10 @@ type validatePossibleDefaultValue<def, k extends keyof def, $, args> =
type nonOptionalKeyFrom<k, $, args> =
parseKey<k> extends PreparsedKey<"required", infer inner> ? inner
: parseKey<k> extends PreparsedKey<"index", infer inner> ?
inferDefinition<inner, $, args> & Key
inferDefinition<inner, $, args> extends infer t ?
// symbols are not constrainable
(t extends of<any, any> ? string : Key) & t
: never
: // "..." is handled at the type root so is handled neither here nor in optionalKeyFrom
// "+" has no effect on inference
never
Expand Down

0 comments on commit fbcdddc

Please sign in to comment.