Automatically applying parse.integer

Let's say I have a type I'm using to validate some API, e.g.
const personSchema = type({
name:'string',
age:'0<=number<=120'
})
const personSchema = type({
name:'string',
age:'0<=number<=120'
})
At some point, I also want to use this type to validate form input. As it's formData,the age will be a string not a number. I know I can do something like this:
const formDataParser=type('parse.formData')
const coerce=type({'age?':'parse.integer'})
const parsedFormInput = formDataParser(formInput).pipe(coerce).pipe(personSchema)
const formDataParser=type('parse.formData')
const coerce=type({'age?':'parse.integer'})
const parsedFormInput = formDataParser(formInput).pipe(coerce).pipe(personSchema)
However, is there a way to somehow automate the coerce step? I have lots of these types (some of them dynamically created) and I'd rather avoid having to manually create an extra type to pick out the numeric fields and parse.integer them. Maybe there's some way I can automatically pick out the number properties from a type and automatically build the coerce step so that parse.integer is only applied to any properties that are numeric in the final schema? I'm fairly new to this, so it could just be my lack of Typescript skill. Any pointers would be much appreciated.
108 Replies
ssalbdivad
ssalbdivad4w ago
Sounds doable but definitely a bit tricky. There's no built-in "coerce" yet. I'd have to think about what the semantics of that would be relative to the existing parse keywords. Where does personSchema come from? Could that be defined in a way where the parsing happens there, and the output could be extracted separately? If you already have the personSchema type to compare with the form input, I guess you could iterate over the keys that way to see which should be converted to numbers
Stuart B
Stuart B4w ago
I'd assumed 'parse.integer' would convert a string to an integer and leave an integer alone, but if fails if the input is already an integer. Would there be a problem with having parse.integer just pass through the existing value if it's already an integer? The personSchema is just a simple example. I have a whole bunch of data objects that can be written to a db, both via an API (where numerics will already be numbers) and forms (where the numerics will be strings). I've built a simple form validator, to which I pass the formData and the arktype type. The validator function uses the passed type to validate the input and passes back the original data and a success message or errors.
I can always deal with this by creating 2 types instead of 1. One is the "API" type and the other one is a parse.integer for the numeric fields and piped to the API type. It just felt a little redundant. Feels like there should be a more elegant way.
ssalbdivad
ssalbdivad4w ago
I don't think I would ever make morphs accept their output types in general just because it's sometimes convenient But I might add some additional keywords to help with that e.g. there is a lift array that converts something to an array if it is not one already so it would be somewhat similar to that I'm sure the right abstractions could help but it's important to strike a balance and not start implementing magical transformations with lots of edge cases as built ins. Ideally they should primarily be simple I/O I think if I were going to add something like this though it would probably be like number.integer.coerced
Stuart B
Stuart B4w ago
Thanks David. In the meantime, if I already have a type() defined, is there any way I can extract the keys of numeric properties from that type? I could then use them to build my parse.integer step.
ssalbdivad
ssalbdivad4w ago
import { type, type Type } from "arktype"
import type { arkKeyOf, getArkKey } from "arktype/internal/keys.ts"

type keysWithNumberValue<o extends object> = {
[k in arkKeyOf<o>]: getArkKey<o, k> extends number ? k : never
}[arkKeyOf<o>] &
unknown

const number = type("number")

const keysWithNumberValue = <t extends Type<object>>(
t: t
): Type<keysWithNumberValue<t["t"]>> =>
t.keyof().internal.distribute(
key => (t.get(key as never).extends(number) ? key : null),
branches =>
t.$.internal.rootNode(branches.filter(branch => branch !== null))
) as never

const t = type({
foo: "number",
bar: "1",
baz: "string"
})

const keys = keysWithNumberValue(t)

console.log(keys.expression)
import { type, type Type } from "arktype"
import type { arkKeyOf, getArkKey } from "arktype/internal/keys.ts"

type keysWithNumberValue<o extends object> = {
[k in arkKeyOf<o>]: getArkKey<o, k> extends number ? k : never
}[arkKeyOf<o>] &
unknown

const number = type("number")

const keysWithNumberValue = <t extends Type<object>>(
t: t
): Type<keysWithNumberValue<t["t"]>> =>
t.keyof().internal.distribute(
key => (t.get(key as never).extends(number) ? key : null),
branches =>
t.$.internal.rootNode(branches.filter(branch => branch !== null))
) as never

const t = type({
foo: "number",
bar: "1",
baz: "string"
})

const keys = keysWithNumberValue(t)

console.log(keys.expression)
A bit tricky to do, especially if you don't know the runtime type system but this seems to wrok
Stuart B
Stuart B4w ago
Thanks! And having read that, there's also the added bonus that I no longer feel dumb for not knowing how to do it myself 🤣
ssalbdivad
ssalbdivad4w ago
Yeah like I said definitely not the most trivial thing either at a type-level or at runtime haha
PIat
PIat3w ago
I have the same use case
PIat
PIat3w ago
I would like to achieve such behavior
No description
ssalbdivad
ssalbdivad3w ago
Can't you just write a morph?
PIat
PIat3w ago
Ooooh, you updated docs!
ssalbdivad
ssalbdivad3w ago
I added a few new sections. I also have some good abstractions for showing multiple definition types
PIat
PIat3w ago
Oh righttttttt.... So this forum is about doing it inherently Sorry 🙈
ssalbdivad
ssalbdivad3w ago
Yeah, youc ould always just write number.integer|string.parse.integer Which I think is a lot clearer anyways
PIat
PIat3w ago
You could?????
Want results from more Discord servers?
Add your server