Flosi21
Flosi21
Aarktype
Created by Flosi21 on 4/16/2025 in #questions
Discriminated union with union type discriminator gives wrong error messages
Let's say I have this type, right here:
const Thing = type.or({
option: "'A' | 'B'",
a: "string"
}, {
option: "'C' | 'D'",
b: "string"
}, {
option: "'E'",
c: "string"
})

const out = Thing({
option: 'A'
})
const Thing = type.or({
option: "'A' | 'B'",
a: "string"
}, {
option: "'C' | 'D'",
b: "string"
}, {
option: "'E'",
c: "string"
})

const out = Thing({
option: 'A'
})
This will produce the error message a must be a string (was missing), b must be a string (was missing) or c must be a string (was missing), however what one would expect is a must be a string (was missing) Now interestingly this only occurs because when more then ONE of the discriminators is a union. This type here will produce the correct error message a must be a string (was missing)
const Thing = type.or({
option: "'A' | 'B'",
a: "string"
}, {
option: "'C'",
b: "string"
}, {
option: "'E'",
c: "string"
})

const out = Thing({
option: 'A'
})
const Thing = type.or({
option: "'A' | 'B'",
a: "string"
}, {
option: "'C'",
b: "string"
}, {
option: "'E'",
c: "string"
})

const out = Thing({
option: 'A'
})
The why becomes apparent when we take a look at the precompilation, in case 2 it can just switch case over the options
union329Allows(data, ctx) { switch(data?.option) {
case "C": return this.intersection2241Allows(data)
case "E": return this.intersection2247Allows(data)
default: return this.intersection1938Allows(data)
}
return false
},
union329Allows(data, ctx) { switch(data?.option) {
case "C": return this.intersection2241Allows(data)
case "E": return this.intersection2247Allows(data)
default: return this.intersection1938Allows(data)
}
return false
},
where in case 1 it uses the intersectionAllows which cannot tell, that when option = "A" the only remaining field to validate will be a because it checks the WHOLE data instead of just looking at the option and then "locking" in.
union298Allows(data, ctx) { if (this.intersection1938Allows(data)) {
return true
}
if (this.intersection1941Allows(data)) {
return true
}
if (this.intersection1944Allows(data)) {
return true
}
return false
},
union298Allows(data, ctx) { if (this.intersection1938Allows(data)) {
return true
}
if (this.intersection1941Allows(data)) {
return true
}
if (this.intersection1944Allows(data)) {
return true
}
return false
},
Is this a bug? Is there some part of set theory that can justify this behaviour that i do not yet understand? 😅
54 replies
Aarktype
Created by Flosi21 on 4/15/2025 in #questions
type inference resolves types from scopes to never in union
When using types from scopes in a union, arktype will resolve some of them to be "never", however if you insert their definition directly it resolves the type correctly Here is an example. If you paste this in the playground and hover of the out, the type will resolve to "never" in some places. Interestingly the .expression tab in the playground resolves the native Typescript expression correctly Is this a bug or am I misusing scopes in some way?
import { type } from "arktype";

const formScope = type.scope({
"form.number": "string.numeric.parse",
"form.integer": "string.integer.parse",
"form.string": "string > 0",
"form.checkbox": type("true|false|undefined", "=>", (c) => (c === undefined ? false : c)),
"form.date": /^\d{2}\.\d{2}\.\d{4}$/,
"form.year": type(/^\d{4}$/, "=>", (c) => Number.parseInt(c, 10)),
"form.yearmonth": /^\d{2}\/\d{4}$/ ,
"form.yesno.yes": "'yes'",
"form.yesno.no": "'no'",
"form.yesno": ["form.yesno.yes | form.yesno.no", "=>", (c) => c === "yes"],
});

const arkScope = type.scope({
...formScope.export(),
});

const ark = arkScope;

const Thing = ark.type
.or(
ark.type
.or(
{
hasWorkPermit: "form.yesno.yes",
workPermit: {
validUntil: "form.yearmonth",
},
},
{
hasWorkPermit: "form.yesno.no",
},
)
.and({
hasTemporaryResidence: "form.yesno.yes",
temporaryResidence: {
validUntil: "form.yearmonth",
},
}),
{
hasTemporaryResidence: "form.yesno.no",
},
)
.and({
inGermanySince: "form.yearmonth",
});

const out = Thing({})
import { type } from "arktype";

const formScope = type.scope({
"form.number": "string.numeric.parse",
"form.integer": "string.integer.parse",
"form.string": "string > 0",
"form.checkbox": type("true|false|undefined", "=>", (c) => (c === undefined ? false : c)),
"form.date": /^\d{2}\.\d{2}\.\d{4}$/,
"form.year": type(/^\d{4}$/, "=>", (c) => Number.parseInt(c, 10)),
"form.yearmonth": /^\d{2}\/\d{4}$/ ,
"form.yesno.yes": "'yes'",
"form.yesno.no": "'no'",
"form.yesno": ["form.yesno.yes | form.yesno.no", "=>", (c) => c === "yes"],
});

const arkScope = type.scope({
...formScope.export(),
});

const ark = arkScope;

const Thing = ark.type
.or(
ark.type
.or(
{
hasWorkPermit: "form.yesno.yes",
workPermit: {
validUntil: "form.yearmonth",
},
},
{
hasWorkPermit: "form.yesno.no",
},
)
.and({
hasTemporaryResidence: "form.yesno.yes",
temporaryResidence: {
validUntil: "form.yearmonth",
},
}),
{
hasTemporaryResidence: "form.yesno.no",
},
)
.and({
inGermanySince: "form.yearmonth",
});

const out = Thing({})
17 replies
Aarktype
Created by Flosi21 on 4/9/2025 in #questions
Is it normal for error messages / potentially behaviour to change depending on jitless?
I wanted to do some stuff with discriminated unions with jitless: true (due to a bug with jitless that has been fixed) and i noticed that the validation behaviour was different when i set it to jitless: false . Sadlly i didn't manage to reproduce the different validation behaviour but if you paste this into the web playground
const formScope = scope({
"form.number": "string.numeric.parse",
"form.integer": "string.integer.parse",
"form.string": "string > 0"
})

configure({
jitless: true
})

const ark = formScope

const vehicle = ark.type({
type: '"car" | "motorcycle" | "caravan"',
brand: "form.string",
model: "form.string",
buildYear: "form.string",
performance: "form.integer",
performanceUnit: '"kW" | "PS"'
})

const Thing = ark.type.or(
{
purpose: ark.unit("vehicle"),
vehicle: vehicle
},
{
purpose: ark.unit("modernization"),
modernization: "'energetic_modernization' | 'other'"
},
{
purpose: ark.unit("free_use"),
}
)

const out = Thing({
purpose: "modernization",
vehicle: {
type: undefined,
brand: undefined,
model: undefined,
buildYear: undefined,
performance: undefined,
performanceUnit: undefined
}
})
const formScope = scope({
"form.number": "string.numeric.parse",
"form.integer": "string.integer.parse",
"form.string": "string > 0"
})

configure({
jitless: true
})

const ark = formScope

const vehicle = ark.type({
type: '"car" | "motorcycle" | "caravan"',
brand: "form.string",
model: "form.string",
buildYear: "form.string",
performance: "form.integer",
performanceUnit: '"kW" | "PS"'
})

const Thing = ark.type.or(
{
purpose: ark.unit("vehicle"),
vehicle: vehicle
},
{
purpose: ark.unit("modernization"),
modernization: "'energetic_modernization' | 'other'"
},
{
purpose: ark.unit("free_use"),
}
)

const out = Thing({
purpose: "modernization",
vehicle: {
type: undefined,
brand: undefined,
model: undefined,
buildYear: undefined,
performance: undefined,
performanceUnit: undefined
}
})
you will see that the error message differs depending on the jitless setting. Is this intended?
2 replies