Configuring morphs in the scope/module level

I'm working on adding coercion to ArkEnv (issue with full context: https://github.com/yamcodes/arkenv/issues/228), since environment variables (in process.env) are always string | undefined, we want to be able to coerce them. Now:
// Manual conversion required
const env = arkenv({
PORT: type("string").pipe(str => Number.parseInt(str, 10)),
DEBUG: type("string").pipe(str => str === "true")
});
// Manual conversion required
const env = arkenv({
PORT: type("string").pipe(str => Number.parseInt(str, 10)),
DEBUG: type("string").pipe(str => str === "true")
});
Desired:
// Coercion
const env = arkenv({
PORT: "number", // "3000" → 3000
DEBUG: "boolean", // "true" → true
TIMESTAMP: "number.epoch" // "1640995200000" → 1640995200000
MY_INTL "number.integer" // "123" → 123, "123.4" fails
});
// Coercion
const env = arkenv({
PORT: "number", // "3000" → 3000
DEBUG: "boolean", // "true" → true
TIMESTAMP: "number.epoch" // "1640995200000" → 1640995200000
MY_INTL "number.integer" // "123" → 123, "123.4" fails
});
Is there a way to configure ArkType to automatically apply morphs at the scope/module level? I've tried: 1. Overriding the number keyword module, or just the root:
export const $ = scope({
string: type.module(
{
...type.keywords.string,
host,
},
),
number: type.module({
...type.keywords.number,
port,
root: type("string | number", "=>", (str) =>
typeof str === "number" ? str : Number.parseInt(str, 10),
),
}),
boolean,
});
export const $ = scope({
string: type.module(
{
...type.keywords.string,
host,
},
),
number: type.module({
...type.keywords.number,
port,
root: type("string | number", "=>", (str) =>
typeof str === "number" ? str : Number.parseInt(str, 10),
),
}),
boolean,
});
With this approach we either override the entire number or have to address each sub-keyword one by one, since it does not address all sub-keywords (number.epoch, number.integer, etc.) 2. Pre-processing by adding JavaScript logic before ArkType validation, but this feels like an anti-pattern and bypasses ArkType's type system. Ideal Solution Something like:
export const $ = scope({
number: type.module(
{
...type.keywords.number,
port,
},
{
morph: (str) =>
typeof str === "number" ? str : Number.parseInt(str, 10),
},
)
});
export const $ = scope({
number: type.module(
{
...type.keywords.number,
port,
},
{
morph: (str) =>
typeof str === "number" ? str : Number.parseInt(str, 10),
},
)
});
Thanks for any guidance!
GitHub
yamcodes/arkenv
⛵ Typesafe environment variables powered by ArkType - yamcodes/arkenv
3 Replies
ssalbdivad
ssalbdivad6d ago
hmm, there is no way to apply a morph to an entire scope. you could write a utility function that would handle it and do the type-level mapping but the types would be a bit complicated to write. this is probably a case where it's worth either just listing those types explicitly (luckily there aren't very many numeric keywords in arktype) or coming up with a different way to name your env keywords. they are all going to be strings so perhaps there's either a way to name things differently that implies that without getting in the way or some way to have preprocessing like what you're describing although I agree it's not super clear how that would work I don't know maybe the type-level scope mapping this is actually the best the problem is that arktype is very regimented about how types and morphs are defined, and the idea of "coercing" as distinct from a morph is kind of contrary to how the entire library is constructed (rather than explicitly defining inputs and transformation logic like any other morph) if you have ideas about how to define some initial coercion step like this that doesn't feel like bloat in the type system and enables patterns like this for process.env or similar mechanisms for forms, I'm interested. so far when I've thought about it it just doesn't seem like there's an elegant, non hand-wavy way to add that concept to the type system
yam.codes
yam.codesOP6d ago
Maybe I’m thinking about it wrong or “too deeply”. That is, maybe it’s not ArkType’s job to say that that what we define as “boolean” isn’t really a “boolean” but it’s actually a string that needs coercion to a Boolean. Maybe it’s ArkEnvs job to say “we assume you’ll want to treat your envs not only as strings for things like feature flagging and ports, so we’ll do it for you automatically before we hand the values over to your ArkType schema, but you can opt-out of coercion per value if you want” Or something like that. I’ll continue to ponder on it and try different things and see what feel right. For sure - I’ll keep an eye out in case something screams “this could be useful in the core library” and give you a shout
ssalbdivad
ssalbdivad6d ago
yeah those are all the trade offs and concerns I would think about you're definitely on the right track. excited to see what you cook up 🧑‍🍳 definitely think there is value in "number" always actually validating a number rather than implicitly parsing a string and converting it, even in a context where that makes sense like env vars so the challenge then is to figure out a way to make it clear and ergonomic without breaking that model

Did you find this page helpful?