RFC: Retaining Struct Schema with Transformations in `Schema.typeSchema`
Summary
Summary
The user is trying to retain the schema structure of a StructSchema while applying transformations using Schema.typeSchema. They provided a TypeScript function to achieve this but were concerned about its safety and compatibility with PropertySignature.
Key Takeaways
-
Initial Attempt:
- The user provided a function
structTypeSchemato transform aStructSchemawhile retaining its fields. - The function was deemed unsafe because it didn't account for
PropertySignature.
- The user provided a function
-
Safety Improvement:
- A suggestion was made to narrow the constraint to ensure safety:
declare const structTypeSchema: <Fields extends { readonly [x: PropertyKey]: Schema.Schema.All }>( schema: Schema.Struct<Fields> ) => Schema.Struct<{ [K in keyof Fields]: Schema.Schema<Schema.Schema.Type<Fields[K]>> }> -
Compatibility with
PropertySignature:- The user expressed a desire to use the function with
PropertySignatures. - A more refined implementation was provided to handle different AST tags:
import { AST, Schema } from "@effect/schema" import { Record } from "effect" export const structTypeSchema = <Fields extends Schema.Struct.Fields>( schema: Schema.Struct<Fields> ): Schema.Struct<{ [K in keyof Fields]: Schema.Schema<Schema.Schema.Type<Fields[K]>> }> => Schema.Struct(Record.map(schema.fields, (field) => { switch (field.ast._tag) { case "PropertySignatureDeclaration": return Schema.make(AST.typeAST(field.ast.type)) case "PropertySignatureTransformation": return Schema.make(AST.typeAST(field.ast.to.type)) default: return Schema.make(AST.typeAST(field.ast)) } })) as any - The user expressed a desire to use the function with
-
Convenience vs. Tree-Shakeability:
- A suggestion was made to add a static method to
Structfor convenience:
Schema.Struct({ a: Schema.NumberFromString }).typeSchema()- However, this approach might not be tree-shakeable and may not justify the addition due to its specificity.
- A suggestion was made to add a static method to
-
Community Feedback:
- It was suggested to open an issue to gather feedback and assess interest in adding this feature.
Conclusion
The refined function provided meets the user's requirements, and there is a potential for further enhancement based on community feedback.
Discord thread
https://discord.com/channels/795981131316985866/1266533881788502096
could this possibly be cursive as well for nested structs?
A limitation of the implementation in the summary is that the resulting type does not preserve the optionality of keys. This can make composing a little confusing:
const fields = {
optional: S.String.pipe(S.optional),
required: S.String,
}
// const schema: S.Schema<{
// readonly optional?: string | undefined;
// readonly required: string;
// }, {
// readonly required: string;
// readonly optional?: string | undefined;
// }>
const schema = S.Struct(fields)
// type typeSchema = S.Struct<{
// optional: S.Schema<string | undefined, string | undefined, never>;
// required: S.Schema<string, string, never>;
// }>
// --->
// typeSchema = S.Schema<{
// readonly optional: string | undefined;
// readonly required: string;
// }, {
// readonly optional: string | undefined;
// readonly required: string;
// }, never>
const typeSchema = structTypeSchema(schema)
So I instead redefined structTypeSchema using a modified S.Struct.Type
type TypeFields<F extends S.Struct.Fields> =
Types.UnionToIntersection<
Exclude<{
[K in keyof F]: F[K] extends OptionalPropertySignature
? { readonly [H in K]?: S.Schema<S.Schema.Type<F[H]>> }
: { readonly [h in K]: S.Schema<S.Schema.Type<F[h]>> }
}[keyof F], undefined>
// using this instead of `infer Q ? Q : never` because otherwise the resulting
// fields are not usable when passed to functions of the type:
// <Fields extends S.Struct.Fields>(schema: S.Struct<Fields>) => any
// because `never` is not assignable to `S.Struct.Fields`
> extends infer Q ? (Q extends S.Struct.Fields ? Q : {}) : {}
type TypeStruct2<F extends S.Struct.Fields> = S.Struct<TypeFields<F>>
type structTypeSchema2 = <Fields extends S.Struct.Fields>(schema: S.Struct<Fields>) => TypeStruct2<Fields>
const structTypeSchema2 = structTypeSchema as unknown as structTypeSchema2
const typeSchema2 = structTypeSchema2(schema)
const typeSchema2: TypeStruct2<{
optional: S.optional<typeof S.String>;
required: typeof S.String;
}>
Yay!
But no!
const asSchema = S.asSchema(structTypeSchema2(schema))
const typeSchema2: S.Schema<never, {
readonly optional: string | undefined;
readonly required: string;
}, never>
// --->
// type schemaType = never ???
type schemaType = S.Struct.Type<typeof typeSchema2['fields']>
This is because in of S.Struct.Type:
{
[K in keyof F]: F[K] extends OptionalPropertySignature
? { readonly [H in K]?: Schema.Type<F[H]> }
: { readonly [h in K]: Schema.Type<F[h]> }
}[keyof F]
resolves to:
{
readonly optional?: string | undefined;
} | {
readonly required: string;
} | undefined
Which resolves to never when wrapped in Types.UnionToIntersection<>
So in order for this to interoperate smoothly with the rest of schema, I think that S.Struct.Type needs to be updated to include Types.UnionToIntersection<Exclude<..., undefined>>
Actually, my mistake. That would create an invalid schema - I confused the schema optionality with the type optionality. I think that just using this would work:
export type OptionalPropertySignatureWithoutDefault =
| S.PropertySignature<"?:", any, PropertyKey, S.PropertySignature.Token, any, false, unknown>
| S.PropertySignature<"?:", any, PropertyKey, S.PropertySignature.Token, never, false, unknown>
| S.PropertySignature<"?:", never, PropertyKey, S.PropertySignature.Token, any, false, unknown>
| S.PropertySignature<"?:", never, PropertyKey, S.PropertySignature.Token, never, false, unknown>
export type TypeFields<F extends S.Struct.Fields> =
Types.UnionToIntersection<
Exclude<{
[K in keyof F]: {
readonly [H in K]: F[K] extends OptionalPropertySignatureWithoutDefault
? S.optional<S.Schema<S.Schema.Type<F[H]>>>
: S.Schema<S.Schema.Type<F[H]>>
}
}[keyof F], undefined>
> extends infer Q ? (Q extends S.Struct.Fields ? Q : {}) : {}