Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the lib fully typed #9

Open
1 task
p10ns11y opened this issue Dec 6, 2024 · 1 comment
Open
1 task

Make the lib fully typed #9

p10ns11y opened this issue Dec 6, 2024 · 1 comment
Assignees

Comments

@p10ns11y
Copy link
Owner

p10ns11y commented Dec 6, 2024

Tasks

  • Make the library fully typed specially those areas give impact to user developers

Main: type hinting transformSchema

It is dynamic and nested and new schema updated based config

const baseSchema = z.object({
      category: z
        .object({
          name: z.string().optional(),
          subcategories: z
            .array(
              z
                .object({
                  name: z.string().optional(),
                  items: z.array(z.string()).optional(),
                })
                .optional()
            )
            .optional(),
        })
        .optional(),
    });

    const config = {
      category: {
        subcategories: {
          '*': {
            items: true,
          },
        },
      },
    };

 const transformedSchema = transformSchema(baseSchema, config);

Approach

Begin with Grok

  1. Define a Generic Type for the Config:

First, define a generic type that describes the structure of the config. This type should be flexible to handle arbitrary nesting and the special '*' wildcard for applying transformations to all children of a level:

type TransformConfig<T> = {
  [K in keyof T]?: T[K] extends object
    ? TransformConfig<T[K]> | '*'
    : boolean;
};
  1. Define a Generic Type for Schema Transformation:
    Now, create a type that can transform the schema based on the configuration:
type TransformSchema<T, C extends TransformConfig<T>> = {
  [K in keyof T]: T[K] extends object
    ? (C[K] extends '*' 
        ? TransformSchema<T[K], { [P in keyof T[K]]: true }>
        : C[K] extends object
          ? TransformSchema<T[K], C[K]>
          : C[K] extends true
            ? Required<T[K]>
            : T[K])
    : C[K] extends true ? NonNullable<T[K]> : T[K];
};
  1. Function Signature:
    Implement transformSchema with these types:
function transformSchema<T extends z.ZodObject<any>, C extends TransformConfig<z.infer<T>>>(schema: T, config: C): z.ZodObject<TransformSchema<z.infer<T>, C>> {
  // Implementation would involve recursively walking through the schema 
  // and altering the options based on config. This part is simplified:
  return schema as any; // Placeholder for actual transformation logic
}

Grok

Fine tune with Claude (free version)

import { z } from 'zod';

// Enhanced configuration type with more flexible typing
type TransformConfig = {
  [key: string]: boolean | { '*'?: TransformConfig } | TransformConfig;
};

// Utility type to safely extract nested configuration
type ExtractNestedConfig<T> = 
  T extends { '*': infer U } 
    ? U extends TransformConfig 
      ? U 
      : {} 
    : {};

// Type-safe schema transformation type
type TransformSchemaType<
  TSchema extends z.ZodType,
  TConfig extends TransformConfig
> = TSchema extends z.ZodObject<infer Shape>
  ? z.ZodObject<{
      [K in keyof Shape]: K extends keyof TConfig
        ? TConfig[K] extends true
          ? Shape[K] extends z.ZodOptional<infer U> 
            ? U 
            : Shape[K]
        : TConfig[K] extends false
          ? z.ZodOptional<Shape[K]>
          : TConfig[K] extends { '*': infer NestedConfig }
            ? Shape[K] extends z.ZodArray<infer Item>
              ? z.ZodOptional<
                  z.ZodArray<
                    TransformSchemaType<
                      Item, 
                      ExtractNestedConfig<TConfig[K]>
                    >,
                    'many'
                  >
                >
              : Shape[K]
            : Shape[K]
        : Shape[K]
    }>
  : TSchema;

// Transformation function with improved config handling
function transformSchema<
  TSchema extends z.ZodType,
  TConfig extends TransformConfig
>(
  schema: TSchema, 
  config: TConfig
): TransformSchemaType<TSchema, TConfig> {
  if (schema instanceof z.ZodObject) {
    const shape = schema.shape;
    const newShape: Record<string, z.ZodType> = {};

    (Object.keys(shape) as Array<keyof typeof shape>).forEach(key => {
      const value = shape[key];
      const keyConfig = config[key as string];

      if (keyConfig === true) {
        newShape[key as string] = value.optional() instanceof z.ZodOptional 
          ? value.unwrap() 
          : value;
      } else if (keyConfig === false) {
        newShape[key as string] = value.optional();
      } else if (typeof keyConfig === 'object' && keyConfig['*']) {
        if (value instanceof z.ZodArray) {
          const itemSchema = value.element;
          const arrayConfig = keyConfig['*'];
          
          // Safe extraction of nested configuration
          const nestedConfig = 
            typeof arrayConfig === 'object' 
              ? arrayConfig 
              : {};
          
          const transformedItemSchema = Object.keys(nestedConfig).length > 0
            ? transformSchema(itemSchema, nestedConfig)
            : itemSchema;
          
          newShape[key as string] = z.array(transformedItemSchema).optional();
        }
      } else {
        newShape[key as string] = value;
      }
    });

    return z.object(newShape) as TransformSchemaType<TSchema, TConfig>;
  }

  return schema as TransformSchemaType<TSchema, TConfig>;
}

// Complex Schema Definition
const complexSchema = z.object({
  users: z.array(
    z.object({
      id: z.string(),
      profile: z.object({
        name: z.string().optional(),
        contacts: z.array(
          z.object({
            type: z.string(),
            value: z.string().optional()
          }).optional()
        ).optional()
      }).optional()
    }).optional()
  ).optional(),
  settings: z.object({
    theme: z.string().optional(),
    notifications: z.object({
      email: z.boolean().optional(),
      push: z.boolean().optional()
    }).optional()
  }).optional()
});

// Configurations
const config1 = {
  users: {
    '*': {
      profile: {
        contacts: {
          '*': {
            value: true // Make contact value required
          }
        }
      }
    }
  }
} satisfies TransformConfig;

const config2 = {
  settings: {
    notifications: {
      email: true,  // Make email notification required
      push: false   // Make push notification optional
    }
  }
} satisfies TransformConfig;

// Transformations with explicit generic type hints
const transformedSchema1 = transformSchema<typeof complexSchema, typeof config1>(
  complexSchema, 
  config1
);

const transformedSchema2 = transformSchema<typeof complexSchema, typeof config2>(
  complexSchema, 
  config2
);

// Type inference demonstration
type OriginalType = z.infer<typeof complexSchema>;
type TransformedType1 = z.infer<typeof transformedSchema1>;
type TransformedType2 = z.infer<typeof transformedSchema2>;

Still didn't solve the problem and nested object still optional

Claude:

Image

Local attempt with simple type hints:

Image

@p10ns11y p10ns11y self-assigned this Dec 6, 2024
@p10ns11y p10ns11y converted this from a draft issue Dec 6, 2024
@p10ns11y
Copy link
Owner Author

p10ns11y commented Jan 16, 2025

Instead of solving this, it also make sense to let the user overwrite type-hints matching config overwrites (prone to human errors if the config overwrites are complex).

Image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Ready
Development

No branches or pull requests

1 participant