A
arktype4mo ago
PIat

Converting Zod to Arktype

Hello! I'm really interested in using Conform instead of React Hook Form for better DX and server-side capabilities. However, it doesn't have Ark support yet, so I'll have to create my own simple patch. Are there by any chance some examples or comparisons between Zod and Arktype architectures to go off of when translating these files, namely to extract the name of the constraints like Array in runtime? expression might be "key"? pun intended https://github.com/edmundhung/conform/blob/main/packages/conform-zod/constraint.ts https://github.com/edmundhung/conform/blob/main/packages/conform-zod/parse.ts Also, is there an way to do something like superRefine for custom run-time conditions? https://zod.dev/?id=superrefine
87 Replies
ssalbdivad
ssalbdivad3mo ago
Sorry I missed this. It looks like you've figured out some of this. You can iterate over the errors and check .code to check the kind of error it was. It has lots of additional introspectable information about the type of error. superRefine is just a long-winded narrow 😅
PIat
PIatOP3mo ago
Ah, thank you! I'm slowly starting to understand morphs and constraints I've been looking through the tests, and got a bunch of questions answered The possiibilities are endless 🤯
ssalbdivad
ssalbdivad3mo ago
I appreciate your diligence! Unit tests definitely the best place to see comprehensive docs at the moment haha
PIat
PIatOP3mo ago
Yes, I in turn appreciate your thoroughness in writing the tests, it's really helpful
ssalbdivad
ssalbdivad3mo ago
It's also helpful for making sure everything works 😅
PIat
PIatOP3mo ago
But I couldn't find an example on how to create/edit types on runtime 🥹
const keysWithNumberValue = <T extends Type<object>>(t: T): T => {
let res = {}

t.keyof().internal.distribute((key) => {
let tt = t.get(key as never)
if (tt.extends(type.number))
return (res[key] = type('number|string').pipe((v) => Number(v)))

return (res[key] = tt.in)
}) as never

return type(res)
}

const userType = keysWithNumberValue(
type({
userName: 'string',
age: 'number',
}),
)

const out = userType({
userName: 'my-name',
age: '123',
})

if (out instanceof ArkErrors) console.log(out.summary)

/**
value at ["\"age\""] must be a number or a string (was missing)
value at ["\"userName\""] must be a string (was missing)
*/
const keysWithNumberValue = <T extends Type<object>>(t: T): T => {
let res = {}

t.keyof().internal.distribute((key) => {
let tt = t.get(key as never)
if (tt.extends(type.number))
return (res[key] = type('number|string').pipe((v) => Number(v)))

return (res[key] = tt.in)
}) as never

return type(res)
}

const userType = keysWithNumberValue(
type({
userName: 'string',
age: 'number',
}),
)

const out = userType({
userName: 'my-name',
age: '123',
})

if (out instanceof ArkErrors) console.log(out.summary)

/**
value at ["\"age\""] must be a number or a string (was missing)
value at ["\"userName\""] must be a string (was missing)
*/
This seemed to get me far enough, but then when I acually use it, it thinks all the values are missing If there is a specific test or discussion about it, I'd gladly read it, I just couldn't find the proper keywords to find it
ssalbdivad
ssalbdivad3mo ago
This kind of internal type mapping stuff isn't really a big focus of the documentation yet. It mirrors a lot of operations in TS, but there's a ton of nuance to the way unions, structures etc. are handled (like in TS) Here's a cleaner implementation though:
const keysWithNumberValue = <T extends Type<object>>(t: T): T =>
t.keyof().internal.distribute(
key => {
if (!key.hasKind("unit")) {
throw new Error("Index signatures cannot be mapped")
}

const k: Key = key.unit as never

let value: Type = t.get(key as never)
return [
k,
(value.extends(type.number) ?
value.or("string.numeric.parse")
: value) as Type
]
},
entries => type(Object.fromEntries(entries))
)
const keysWithNumberValue = <T extends Type<object>>(t: T): T =>
t.keyof().internal.distribute(
key => {
if (!key.hasKind("unit")) {
throw new Error("Index signatures cannot be mapped")
}

const k: Key = key.unit as never

let value: Type = t.get(key as never)
return [
k,
(value.extends(type.number) ?
value.or("string.numeric.parse")
: value) as Type
]
},
entries => type(Object.fromEntries(entries))
)
It will be nice when there is a mapped type abstraction built around this Maybe I will add it now
PIat
PIatOP3mo ago
So the first callback if for iterating the keys, and the second callback is for building the result object, which distribute returns?
ssalbdivad
ssalbdivad3mo ago
Yeah
PIat
PIatOP3mo ago
Got ittttt....... It works ;-;-;-;-;-; That makes me so happy I bashed my head on the wall for about 4 hours with this yesterday 😆
ssalbdivad
ssalbdivad3mo ago
Haha yeah it helps to know how the type system works
PIat
PIatOP3mo ago
Do you know why this acted like it did?
ssalbdivad
ssalbdivad3mo ago
Your biggest problem is that you were treating key like a literal key but it was a node So I guess it gets converted to a string using .expression
PIat
PIatOP3mo ago
And the actual literal is key.unit
ssalbdivad
ssalbdivad3mo ago
Yeah
PIat
PIatOP3mo ago
Well, the object always had the proper shape actually age and userName was on it properly
ssalbdivad
ssalbdivad3mo ago
I think you were just thinking it did because it looked similar but those strings were actualy quoted Because that is how the literal "age" would be converted to an expression "age" So it would look for the key in quotes
PIat
PIatOP3mo ago
Ohhhhhh OHHHHH ;-;-;-;-;-;-;-;-;-; I'm laughing so hard right now Got it, thank you
ssalbdivad
ssalbdivad3mo ago
Glad you're not crying 😅
PIat
PIatOP3mo ago
I'm really thankful you showed me the proper solution, so I can move forward, nothing to cry about
ssalbdivad
ssalbdivad3mo ago
It will be a lot easier once I add an API for mapped types You shouldn't have to think about those structural nuances externally
PIat
PIatOP3mo ago
How would I theoretically be able to know that the key was actually in unit? Dimava the Monoreaper understands the library well, how come?
ssalbdivad
ssalbdivad3mo ago
I don't know he's spent a lot of time answering questions I guess haha
ssalbdivad
ssalbdivad3mo ago
You can see from the type key will not be a string
No description
ssalbdivad
ssalbdivad3mo ago
There's a brief summary of the type node structure here: https://github.com/arktypeio/arktype/blob/8e3c9ec1fc4aaa269b2a36f3c32bdc16ab889c83/ark/schema/README.md So you can see what each node does. unit represents a single value You can use .hasKind on an internal node to narrow it based on its kind I guess I could have also just .assertHasKind("unit") there Oh that explanation kinda skips morphs haha I guess that's why I said "the part that exists in TS" Morphs are between unions and intersections
PIat
PIatOP3mo ago
So every "type" has a different unit?
ssalbdivad
ssalbdivad3mo ago
No only types that represent a literal value have a unit
PIat
PIatOP3mo ago
Ahhhhh
ssalbdivad
ssalbdivad3mo ago
So the key "age" represents exactly one value in JS So it has a unit node Or it is a unit node rather
PIat
PIatOP3mo ago
Riggght
ssalbdivad
ssalbdivad3mo ago
"age"|"name"would be a union of two unit nodes "age" | "name" | symbol would be a union of two unit nodes and a domain node
PIat
PIatOP3mo ago
And domain is the keyword that comes out when the type was wrong I know that from the errors 😁
ssalbdivad
ssalbdivad3mo ago
Domain is similar to typeof But adapted to the TS keywords, so object includes function but not null It's basically all the lowercase primitive keywords TS provides Except that in the type system, it only includes the non-enumerable domains, i.e. string number bigint symbol and object The others are represented as unit nodes So e.g. boolean is actually a union of two unit nodes But having it so that there is only one normalized representation for a given type means we can accurately compare them
PIat
PIatOP3mo ago
Okaaaaaaaaaay So domain, proto and unit live on the same level Then they can be joined into unions
ssalbdivad
ssalbdivad3mo ago
Yeah because they will never coexist
PIat
PIatOP3mo ago
This is so profound and cool
ssalbdivad
ssalbdivad3mo ago
The easiest way to see how any of your types are structured is to use .json
PIat
PIatOP3mo ago
Thinking about this is so fun
ssalbdivad
ssalbdivad3mo ago
Yes it's a really beautiful problem. Most of the work on the codebase was on the type system, I'm excited for more people to learn about it
PIat
PIatOP3mo ago
Hehe, I got experience with that 😆 Me too
ssalbdivad
ssalbdivad3mo ago
It's basically a pure, precise version of what TS approximates That is also extended to include runtime constraints and morphs
PIat
PIatOP3mo ago
Which is just wild
PIat
PIatOP3mo ago
Like hoooooow
No description
PIat
PIatOP3mo ago
It's a string with quotation marks, how does it knoooowwww
ssalbdivad
ssalbdivad3mo ago
Because it parses it first haha
PIat
PIatOP3mo ago
But that's crazyyyy
ssalbdivad
ssalbdivad3mo ago
It has to otherwise narrow would never work
PIat
PIatOP3mo ago
Every time I use Arktype, I feel like it's not real
ssalbdivad
ssalbdivad3mo ago
I'm surprised you're not using the extension it makes types like that a lot more readable
PIat
PIatOP3mo ago
I only use dark theme when sending screenshots to you 😅
ssalbdivad
ssalbdivad3mo ago
You don't need to use the theme though to benefit from the extension It's called ArkDark but it also provides syntax highlighting You can use it with whatever theme you want
PIat
PIatOP3mo ago
No description
PIat
PIatOP3mo ago
Woooooooo Wait, it doeeessssss No waayyyy I thought it was the theme that did it ;-; how Also, did you think of the color palette for ArkDark yourself? Thank you for the tip 🙏 🙏 🙏 🙏
ssalbdivad
ssalbdivad3mo ago
Yeah it is kind of based on the palette I came up with for the website/logo though
PIat
PIatOP3mo ago
I'm happy to announce the Conform parsing does work fine in my simple use-cases with this!
ssalbdivad
ssalbdivad3mo ago
Looks promising! So are you publishing this as a package?
PIat
PIatOP3mo ago
I would definitely like to, it covers the simplest use case I even use it with the shady-ark-i18n to translate the messages
onValidate({ formData }) {
let res = parseWithArktype(formData, {
def,
formatError: (out) =>
toArkI18n(out).reduce((acc, curr) => {
return {
...acc,
[curr.path[curr.path.length - 1]]: {
code: curr.code,
data: curr.data,
},
}
}, {}),
})
onValidate({ formData }) {
let res = parseWithArktype(formData, {
def,
formatError: (out) =>
toArkI18n(out).reduce((acc, curr) => {
return {
...acc,
[curr.path[curr.path.length - 1]]: {
code: curr.code,
data: curr.data,
},
}
}, {}),
})
ssalbdivad
ssalbdivad3mo ago
I already know a few teams that want to use it with conform, I'd honestly accept a PR for the main repo if you wanted to add it as an ecosystem package
PIat
PIatOP3mo ago
From what I can see, Conform has all the packages listed in the repo like this: https://github.com/edmundhung/conform/tree/main/packages
ssalbdivad
ssalbdivad3mo ago
True that would be better You should submit it as a PR there then instead I suppose
PIat
PIatOP3mo ago
Hard to say
ssalbdivad
ssalbdivad3mo ago
Well you should definitely submit it there, whether they accept it is their choice
PIat
PIatOP3mo ago
It's still missing the constraint part to automatically add min, max etc. to fields But it could get the ball rolling
ssalbdivad
ssalbdivad3mo ago
Yeah we'd want to make sure it's complete first We could always add it to the arktype repo for now until it's complete and tested then submit a PR
PIat
PIatOP3mo ago
Yes, let's do that As I said, I never contributed like this, so you know better 😁
ssalbdivad
ssalbdivad3mo ago
To be fair I still want it to be at least tested if not complete before adding it to the repo 😛 But if you start integrating it with the arktype repo you can use the same test patterns
PIat
PIatOP3mo ago
Ooooh My first unit tests 😅 Okay, will do 💪
Stuart B
Stuart B3mo ago
@PIat Thanks for pointing me at this. I'm not using conform but this helps me with my problem. One comment... I changed ? value.or('string.numeric.parse') to type('string.numeric.parse').pipe(value). Otherwise, if you have something like {age:'number<100'} and pass it a string like '101', it correcty parses to a number but the >100 constraint is lost. Also... I'm a bit puzzled. I can't get the check for a boolean type to work. I have a boolean property in my type but it's not getting picked up by value.extends('boolean') or value.extends(type.boolean). I assume it worked for you? If I use value.overlaps('boolean'), it works ok. But I'm not sure if that's wise?
ssalbdivad
ssalbdivad3mo ago
It's not lost, constraints apply to the input so string.numeric.parse < 100 means a string less than 100 characters parsed into a number There is actually an API I'm thinking about for withOut that allows you specifically to chain constraints on your output, but it's really just sugar over pipe as you said Are you sure your expectations align with how TS handles this for unions? What comparison specifically do you want to work
Stuart B
Stuart B3mo ago
Below is the code I'm working with (and forgive me, I'm not exactly an experienced dev)
function formify<T extends Type<object>>(validator: T): T {
return validator.keyof().internal.distribute(
(key) => {
if (!key.hasKind('unit')) {
throw new Error('Index signatures cannot be mapped');
}
const unit = key.unit as never;
const value: Type = validator.get(key as never);

let new_value: Type;
switch (true) {
case value.extends(type.number):
console.log('Number field');
// This picks up number values in the original type and adds a parse string in front
// Any original constrints on the number are still applied
new_value = type('string.numeric.parse').pipe(value);
break;
case value.overlaps('boolean'):
// NOTE: If I use .extends here it does not pick up the field and the boolean field drops through to default.
// This picks up boolean values, changes to a string and then morphs 'on' back to true.
console.log('Boolean field');
new_value = type('string').pipe((text) => (text === 'on' ? true : false));
break;
default:
console.log('any other type of field');
new_value = value;
}
return [unit, new_value];
},
(entries) => type(Object.fromEntries(entries))
);
}
function formify<T extends Type<object>>(validator: T): T {
return validator.keyof().internal.distribute(
(key) => {
if (!key.hasKind('unit')) {
throw new Error('Index signatures cannot be mapped');
}
const unit = key.unit as never;
const value: Type = validator.get(key as never);

let new_value: Type;
switch (true) {
case value.extends(type.number):
console.log('Number field');
// This picks up number values in the original type and adds a parse string in front
// Any original constrints on the number are still applied
new_value = type('string.numeric.parse').pipe(value);
break;
case value.overlaps('boolean'):
// NOTE: If I use .extends here it does not pick up the field and the boolean field drops through to default.
// This picks up boolean values, changes to a string and then morphs 'on' back to true.
console.log('Boolean field');
new_value = type('string').pipe((text) => (text === 'on' ? true : false));
break;
default:
console.log('any other type of field');
new_value = value;
}
return [unit, new_value];
},
(entries) => type(Object.fromEntries(entries))
);
}
const person = type({
name: 'string',
age: 'number>18',
height: 'number<300',
'is_male?': 'boolean'
});

const fake_formData_1 = {
// This works
name: 'Stuart',
age: '40',
height: '180',
is_male: 'on'
};

const fake_formData_2 = {
// This errors.
name: 'Stuart',
age: '40',
height: '180'
}; // It seems that the formify function is converting any optional keys into required.
// And I think I'm too dumb to figure out why!

const person_form_validator = formify(person);

const validated_1 = person_form_validator(fake_formData_1);
console.log('validated_1:');
if (validated_1 instanceof ArkErrors) console.log(validated_1.summary);
else console.log(validated_1);

const validated_2 = person_form_validator(fake_formData_2);
console.log('validated_2:');
if (validated_2 instanceof ArkErrors) console.log(validated_2.summary);
else console.log(validated_2);
const person = type({
name: 'string',
age: 'number>18',
height: 'number<300',
'is_male?': 'boolean'
});

const fake_formData_1 = {
// This works
name: 'Stuart',
age: '40',
height: '180',
is_male: 'on'
};

const fake_formData_2 = {
// This errors.
name: 'Stuart',
age: '40',
height: '180'
}; // It seems that the formify function is converting any optional keys into required.
// And I think I'm too dumb to figure out why!

const person_form_validator = formify(person);

const validated_1 = person_form_validator(fake_formData_1);
console.log('validated_1:');
if (validated_1 instanceof ArkErrors) console.log(validated_1.summary);
else console.log(validated_1);

const validated_2 = person_form_validator(fake_formData_2);
console.log('validated_2:');
if (validated_2 instanceof ArkErrors) console.log(validated_2.summary);
else console.log(validated_2);
My points/questions: 1. In the examples above in this thread, the "detection" of a Boolean field in the type is done by checking value.extends('boolean'). For some reason I don't understand, that doesn't work here but value.overlaps('boolean') does. My assumption here is that I'm being an idiot, but I can't work out where. 2. In Plat's conform parser I see that value.or('string.numeric.parse') is used for a number field. This works but seems to mean that and numeric constraints that were on the original numeric field definition don't apply when it's parsed from a string. So I tried type('string.numeric.parse').pipe(value), which seems to keen any original numeric constraints. 3. The below works for numbers, but Boolean is causing me problems. The original type has the Boolean as an option (as an unchecked checkbox on a form won't provide a value in formData). But the function is converting the optional key back into required. So it works if a checkbox is checked and passes 'on' but doesn't work if it is unchecked.
ssalbdivad
ssalbdivad3mo ago
The reason is the field is optional so it's actually boolean|undefined when you .get it You can always use stuff like console.log(value.expression) if you want to see what your actual type looks like There's a lot of introspectability One option would be to do:
const value: Type = validator.get(key as never).exclude("undefined");
const value: Type = validator.get(key as never).exclude("undefined");
.expression essentially converts any ArkType to an expanded TS syntax The issue with optional and required is as bit trickier because you're just iterating over the literal keys. keyof does not preserve associations with required or optional
Stuart B
Stuart B3mo ago
Is there an easy(ish) way that I can force any boolean to be optional in the new type? (Because for this use case, it always will be, as I'll receive 'on' or nothing at all)
ssalbdivad
ssalbdivad3mo ago
As a heuristic I'd say you could likely check as you're iterating over the keys:
return [type.undefined.extends(value) ? `${unit}?` : unit, new_value];`
return [type.undefined.extends(value) ? `${unit}?` : unit, new_value];`
I will add a wrapper API like .map around this. Even without native mapped type syntax, being able to map entries would be way easier externally if I do this
Stuart B
Stuart B3mo ago
Sounds good. And once again, thanks for the help! I'm stuck again! (It's getting a bit embarrassing now, sorry David!). This is a section of my current code.
const key_name = key.unit as never;
const value: Type = validator.get(key as never);
const non_optional_value = value.exclude('undefined');
let new_value: Type;
switch (true) {
case non_optional_value.extends(type.number):
console.log('Number field');
new_value = type('string.numeric.parse').pipe(value);
break;
case non_optional_value.extends('boolean'):
console.log('Boolean field');
new_value = type('string')
.pipe((text) => (text === 'on' ? true : false))
.pipe(value);
break;
default:
console.log('any other type of field');
new_value = non_optional_value;
}
const key_name = key.unit as never;
const value: Type = validator.get(key as never);
const non_optional_value = value.exclude('undefined');
let new_value: Type;
switch (true) {
case non_optional_value.extends(type.number):
console.log('Number field');
new_value = type('string.numeric.parse').pipe(value);
break;
case non_optional_value.extends('boolean'):
console.log('Boolean field');
new_value = type('string')
.pipe((text) => (text === 'on' ? true : false))
.pipe(value);
break;
default:
console.log('any other type of field');
new_value = non_optional_value;
}
I then pass this type through the function to get a modified type that is used to validate form input.
const person = type({
'name?': 'string="Dave"',
'age?': 'number>18=19',
height: 'number<300',
'is_male?': 'boolean=true'
});
const person = type({
'name?': 'string="Dave"',
'age?': 'number>18=19',
height: 'number<300',
'is_male?': 'boolean=true'
});
If I then try to validate this data:
const fake_formData_1 = {
name: 'stuart',
age: '50',
height: '180'
//is_male: ''
};
const fake_formData_1 = {
name: 'stuart',
age: '50',
height: '180'
//is_male: ''
};
I run into a problem with the default. The default for 'name' works ok. 'name' is a string input in both the original and modified types. But the defaults for age and is_male get lost, even though I have pipe the string through original with .pipe(value). That .pipe(value) is retaining the >18 number constraint on age but losing the =19. I've tried adding .default() into the function but it makes no difference. What am I missing? So what's happening is that if I don't have a 'is_male' (e.g. a checkbox is not checked) then the default is not being applied to an optional field so I end up with no 'is_male' value in the output, when I need false.
ssalbdivad
ssalbdivad3mo ago
See the discussion at https://github.com/arktypeio/arktype/issues/1089 There is a plan to optimize this internally to handle this case https://github.com/arktypeio/arktype/issues/1090 I think what you'd need to do here is map the default value internally as well. I should really just finish this .map API which would help a lot 😅 Going from just learning the library to internal type transforms like this is a bit tricky
PIat
PIatOP3mo ago
Hey, you should look into camelCase. It'll be easier on the eyes for you, especially if you start integrating more libraries
Stuart B
Stuart B3mo ago
I know it's controversial but I find snake_case easier to read. I agree camelCase somehow looks better and is the js standard, but I find it easier to follow my code using snake_case. It's sort of the opposite of 1089. I can apply the morph ok but the defaults on the initial type get ignored. Is that the same cause?
ssalbdivad
ssalbdivad3mo ago
yeah not exactly the same but definitely related. Your case is a bit more unique because it involes mapping the defaults themselves If you think about it, to be able to handle that directly, we'd need to know how an arbitrary default provided by the user would translate to the default for your input value, so you'd have to write that logic the default of a node is accessible if there is one as meta.default
Stuart B
Stuart B3mo ago
I'm still missing something. The below is the simplest example of what I want to do. This works, but it requires me to generate 2 preprocessing types, one to make sure there is a string value present and the second to transform the string value to a Boolean. I can't get my head around why that can't be combined into one preprocessing step.
It seems like adding the 'string="off"' default to the second preprocess type should work, but it just ignores it. And I can't put the logic in the morph, as the morph doesn't run if it doesn't get a string value. Maybe I just have to accept "that's just the way it is, deal with it"!
// This is the shape of the data I need. I might use it for other things as well as form input.
const validator = type({
bool_value: 'boolean'
});

// These two preprocessors are just turning what I get from an HTML form checkbox into boolean
const pre_process_1 = type({
bool_value: type('string="off"')
});
const pre_process_2 = type({
bool_value: type('string').pipe((v) => (v === 'on' ? true : false))
});

const data = {
// bool_value does not exist, so I want it to be false
// if bool_value='on' then I need true. Otherwise false.
};

const preprocess_1_result = pre_process_1(data);
if (preprocess_1_result instanceof type.errors) console.log(preprocess_1_result.summary);
else console.log('preprocess 1:', preprocess_1_result);

const preprocess_2_result = pre_process_2(preprocess_1_result);
if (preprocess_2_result instanceof type.errors) console.log(preprocess_2_result.summary);
else console.log('preprocess 2:', preprocess_2_result);

const result = validator(preprocess_2_result);
if (result instanceof type.errors) console.log(result.summary);
else console.log('final:', result);
// This is the shape of the data I need. I might use it for other things as well as form input.
const validator = type({
bool_value: 'boolean'
});

// These two preprocessors are just turning what I get from an HTML form checkbox into boolean
const pre_process_1 = type({
bool_value: type('string="off"')
});
const pre_process_2 = type({
bool_value: type('string').pipe((v) => (v === 'on' ? true : false))
});

const data = {
// bool_value does not exist, so I want it to be false
// if bool_value='on' then I need true. Otherwise false.
};

const preprocess_1_result = pre_process_1(data);
if (preprocess_1_result instanceof type.errors) console.log(preprocess_1_result.summary);
else console.log('preprocess 1:', preprocess_1_result);

const preprocess_2_result = pre_process_2(preprocess_1_result);
if (preprocess_2_result instanceof type.errors) console.log(preprocess_2_result.summary);
else console.log('preprocess 2:', preprocess_2_result);

const result = validator(preprocess_2_result);
if (result instanceof type.errors) console.log(result.summary);
else console.log('final:', result);
Hi @ArkDavid , just to close this off, is the above example the way I need to go with this, or is there an approach that doesn't require 2 preprocess steps? I'm happy with either answer. And sorry for my ignorance, if that's what it is. You're doing a very impressive job with Arktype!
ssalbdivad
ssalbdivad3mo ago
Oh yeah sorry I forgot to follow up on this. I know this default mapping stuff is tricky to handle externally. Let me merge this new map API maybe that will help haha Looking into this now Honestly this behavior sucks I'm going to fix it
Stuart B
Stuart B3mo ago
It is such a relief that it wasn't me being an idiot! (and thanks)
ssalbdivad
ssalbdivad3mo ago
Definitely not. On the one hand yes generally defaults play kind of a weird role when you also have transforms, but in this case it's not just awkward, the behavior is blatantly wrong. It asks for the output type as a default value but then rejects it at runtime. Should be easy to fix though All right there were a couple edge cases related to morphs that made it a bit more work than I anticipated but hopefully everything "just works" now. The default is actually precomputed as soon as you instantiate your validator if you morph it, so you skip the transform logic altogether from "off" and go straight to false at runtime 🎉 I've added both of these forms to unit tests, and hopefully the logic is developed in a way where however/whenver you add a default input, it should work out 🙏
const processForm = type({
bool_value: type("string='off'").pipe(v => (v === "on" ? true : false))
})

const processForm2 = type({
bool_value: type("string")
.pipe(v => (v === "on" ? true : false))
.default("off")
})
const processForm = type({
bool_value: type("string='off'").pipe(v => (v === "on" ? true : false))
})

const processForm2 = type({
bool_value: type("string")
.pipe(v => (v === "on" ? true : false))
.default("off")
})
(this is in 2.0.0-rc.10)
PIat
PIatOP3mo ago
you skip the transform logic altogether from "off" and go straight to false at runtime
I fail to understand how the transform logic can be skipped. If a string value ("on" or "off") is passed, then it does have to go though .pipe first, right?
Stuart B
Stuart B3mo ago
That's great David, thanks! I think it would be worth clearly mentioning in the docs that defaults refer to the input type, regardless of where you assign them. It's pretty obvious with 'string="off"', but if you add a .default() after the morph, I expect some people will assume that it applies to the morphed type. Also, something like type('string|boolean=false') doesn't work. If there is no value passed then false as a default is not applied. If that's intentional (and I can see why it's a weird thing to do), then should it give a type error in the editor, rather than just ignore it?
ssalbdivad
ssalbdivad3mo ago
Yeah I need to elaborate a bit on that, but yes it's intentional. Initially, defaults could only be added within an object. I loosened that restriction to allow defaults to be specified as metadata so that if they were ever attached to an object, they'd then have the default value. But if it just made the type accept undefined (which I've thought about) it would cause all sorts of inconsistencies in the type system defaults and .optional both basically attach metadata that is used when the type is referenced in an object, not change the type itself There's a broader set of rules that essentialy any constraint you apply without .pipe or .narrow applies to the input. If you think about it it wouldn't really make sense any other way because usually if you just return something from a function we don't have any runtime representation to constrain But also all the .default types should enforce that whatever you pass is their input, so in most cases you'd just get an error if you did something like .default(false) in that case
ssalbdivad
ssalbdivad3mo ago
It has to go through pipe, but it goes through pipe during compilation, so only once. Whenever you actually use it to validate data, it just skips right to the computed result and attaches it to the data.
No description
PIat
PIatOP3mo ago
Ohhhhhh! 😵 Thanks
Want results from more Discord servers?
Add your server