Closed JacKyDev closed 3 weeks ago
We have to distinguish between the schema object (what a schema function returns) and the parsing output. Valibot's schema objects are already readonly by default. So there is nothing extra to do on your side. Parsing output is readonly if you use our readonly
action in the pipe
of a schema. See this API reference for more details and examples: https://valibot.dev/api/readonly/
I understood that so far. My question is rather whether it's
possible to define a schema without readonly
and then make this
schema readonly
afterwards, following the principle:
import * as v from 'valibot';
const Schema = v.object({
entry: v.object({
entry2: v.object({
...
})
})
});
const ReadonlySchema = deepReadonly(Schema);
The result would be that all entries and subentries would then
have readonly
, making both the schema and the output marked
as readonly
in TypeScript.
The current action does this only based on the element specified by the developer. My question is whether a function exists or could exist that does this as well, so that libraries can automatically make an external schema readonly.
So instead of:
import * as v from 'valibot';
const Schema = v.pipe(v.object({
entry: v.pipe(v.object({
entry2: v.pipe(v.object({
...
}), v.readonly())
}), v.readonly())
}), v.readonly());
Or alternatively, that you can specify in the generic
that only readonly
schemas are allowed.
const anyMethod = <TSchema extends ReadonlyBaseSchema>(schema: TSchema){
...
}
// error in typescript Schema is not of type ReadonlyBaseSchema
anyMethod(Schema)
// Works all is fine
anyMethod(ReadonlySchema)
This would inform the schema creator via TypeScript
to make everything readonly
, otherwise it won't work.
I'll try again to illustrate roughly what I mean with the two mentioned examples using a small sample:
Example 1 with deepReadonly:
// Method to create as example a Store with a Schema.
const store = <TSchema extends BaseSchema<unknown, unknown, BaseIssue<unknown>>>(schema: TSchema){
const observers = [];
return {
dispatch: (value: InferOutput<TSchema>) => {
// schema converts into readonly schema validation
// value returns any given prop as readonly
const result = safeParse(deepReadonly(schema), value);
if(result.success){
observers.forEach(observer => observer(result.output));
}
}
observe: (observer) => {
observers.push(observer);
}
}
}
const productStore = store(object({
name: string(),
price: object({
currencyIso: string(),
value: number()
})
}))
productStore.observe((value) => {
// typescript mark error for currencyIso as Readonly Props
value.price.currencyIso = "Huhu"
});
By using the function, the incorrectly defined schema is converted into a readonly, making the output readonly in TypeScript.
Example 2 with the type:
// Method to create as example a Store with a Schema.
// Name of extends Type is a Sample
// This ReadonlyBaseSchema expect only Readonly Schemas
const store = <TSchema extends ReadonlyBaseSchema<unknown, unknown, BaseIssue<unknown>>>(schema: TSchema){
const observers = [];
return {
dispatch: (value: InferOutput<TSchema>) => {
// schema converts into readonly schema validation
// value returns any given prop as readonly
const result = safeParse(schema, value);
if(result.success){
observers.forEach(observer => observer(result.output));
}
}
observe: (observer) => {
observers.push(observer);
}
}
}
// Typescript error while not all props are readonly
const productStore = store(object({
name: string(),
price: object({
currencyIso: string(),
value: pipe(number(), readonly())
})
}))
productStore.observe((value) => {
// typescript mark error for currencyIso as Readonly Props
value.price.currencyIso = "Huhu"
});
I hope this explains my idea better. The goal is to determine whether the schema is fully readonly or to make it so if it isn't.
Currently, this is only possible by applying readonly()
to each schema or each property within the schema.
I have solved this in an inelegant way by using a
DeepReadonly
in TypeScript and defining a ts-ignore
because I can't guarantee that readonly
is used for
every property in the schema.
type DeepReadonly<T> = Readonly<{
[K in keyof T]:
T[K] extends (number | string | symbol) ? Readonly<T[K]>
: T[K] extends Array<infer A> ? Readonly<Array<DeepReadonly<A>>>
: DeepReadonly<T[K]>;
}>
type Event<TSchema extends BaseSchema<unknown, unknown, BaseIssue<unknown>>> = DeepReadonly<{
value: InferOutput<TSchema>
}>
// then in dispatch
const result = safeParse(schema, newValue);
if(result.success){
observer({
// @ts-ignore while schema could not guarantee readonly
value: result.output
})
}
My question is rather whether it's possible to define a schema without
readonly
and then make this schemareadonly
afterwards
Yes, this works with pipe
and readonly
. But we don't have a deepReadonly
action at the moment.
import * as v from 'valibot';
const ArraySchema = v.array(v.string());
const ReadonlyArraySchema = v.pipe(ArraySchema, v.readonly());
Or alternatively, that you can specify in the generic that only
readonly
schemas are allowed.
Unfortunately, I am not sure if this is technically possible with our current implementation, but you could recursively check at runtime if every schema contains a pipe with a readonly
action and throw an error if not.
I would change your workaround to:
import { DeepReadonly } from "ts-essentials";
import * as v from 'valibot';
const Schema = v.object({ foo: v.string() });
const output = v.parse(Schema, ['a', 'b', 'c']) as DeepReadonly<
v.InferOutput<typeof Schema>
>;
Yes, you're right. That's something one can live with much better :) Thank you very much.
Hi, I have a question about the schema and readonly.
The action serves the exact purpose. The problem is the need to write it for every property. I want to mention again that I think it's great that this exists.
In the application I'm working on, I want to ensure that data can only be modified by a dedicated process, and all consumers receive a readonly dataset. I also know from the modular approach that you have to be very careful about simply adding things.
I tried to implement it using a DeepReadonly from typing, but the schema didn't want to work with it, which wasn't unexpected. However, this is basically what I'm trying to achieve. I'm not really aiming to
freeze
objects but rather to improve the DX.Additionally, I didn't find anything that valibot offers in this regard.
Now, to my key points :)
1: Is there already a solution that I'm just not finding?
2: Is there, or would it be an idea, to have a function in the generics that can specify that only schemas of type readonly are allowed?
Example here:
Then the developer would still have to define it for each one, but at least they would be blessed with the feedback.
3: Is there, or would it be an idea, to have a function similar to the
pick
function that takes a schema and converts it into areadonly
schema?Example here:
I included both examples because I believe
readonly
would already be somewhat reserved by the action, anddeepReadonly
would better match the term used in the TypeScript world.4: Is there, or would it be an idea, to specify something like this in the configuration of parse?
Then the schema would generally remain open, but the parse would produce readonly output. However, I imagine that could be quite difficult to implement.
I hope I don't sound completely crazy, and that the comments make sense. Sorry for the amount of text.
I'm more than ready to answer any questions and provide feedback.