sinclairzx81 / typebox

Json Schema Type Builder with Static Type Resolution for TypeScript
Other
4.85k stars 155 forks source link

Deep Partial #265

Closed isaac-scarrott closed 1 year ago

isaac-scarrott commented 1 year ago

I was wondering if it was possible to add a deep partial to the project. Potentially though a option on the Partial function or a separate function called DeepPartial as currently it will only make the top level of the object partial and sub objects remain as defined.

For example

const Example = Type.Object({
  id: Type.String(),
  testingOptions: Type.Object({
    isTest: Type.Boolean(),
  }),
});
const PartialExample = Type.Partial(Example);
type PartialExampleType = Static<typeof PartialExample>; // { id?: string | undefined; testingOptions?: { isTest: boolean; } | undefined; }

// Ideal output
// { id?: string | undefined; testingOptions?: { isTest?: boolean | undefined; } | undefined; }

Could look something similar to:

const PartialExample = Type.Partial(Example, { deep: true })
sinclairzx81 commented 1 year ago

@isaac-scarrott Hi!

Unfortunately, TypeBox utility types (such as Type.Partial(T)) are built to align to the default TypeScript Utility Types only. Adding a TypeBox Type.DeepPartial() or (or {deep: true}) option would be considered "out of scope" from a TypeBox perspective (although I'm aware that quite a few other libraries implement this type)

Current Workaround

It should be possible to construct a DeepPartial type using the following.

TypeScript Link Here

import { Type, Static, TObject, TProperties, TUnsafe } from '@sinclair/typebox'

// ---------------------------------------------------------------
// Deep Partial
// ---------------------------------------------------------------

type DeepPartial<T extends Record<any, any>> = {
  [K in keyof T]?: T[K] extends Record<any, any> ? 
    DeepPartial<T[K]> : 
    T[K] 
}

function DeepPartial<T extends TObject>(schema: T): TUnsafe<DeepPartial<Static<T>>> {
  const properties = Object.keys(schema.properties).reduce((acc, key) => {
    const property = schema.properties[key]
    const mapped = (property.type === 'object') ? DeepPartial(property as TObject) : property
    return { ...acc, [key]: Type.Optional(mapped)}
  }, {}) as TProperties
  return Type.Object({ ...properties })
}

// ---------------------------------------------------------------
// Example
// ---------------------------------------------------------------

const Example = Type.Object({
  id: Type.String(),
  testingOptions: Type.Object({
    isTest: Type.Boolean(),
  }),
});

const DeepPartialExample = DeepPartial(Example)

type DeepPartialExample = Static<typeof DeepPartialExample>

Future

There is currently some work / research being done to try and introduce Mapped Typed functionality to TypeBox. As DeepPartial is typically implemented by way of a recursive mapped type, the following would be an example of how TypeBox hopes to see this functionality implemented one day.

// ----------------------------------------------
// TypeBox: Recursive Mapped Type
// ----------------------------------------------

// type DeepPartial<T extends Record<any, any>> = {
//  [K in keyof T]?: T[K] extends Record<any, any> ? 
//    DeepPartial<T[K]> : 
//    T[K] 
// }

const DeepPartial = <T extends TObject>(schema: T) => Type.Mapped(schema, (T, K) => {
   return (TypeGuard.TObject(T[K])) ? 
     Type.Optional(DeepPartial(T[K])) : 
     Type.Optional(T[K])          
})

// ----------------------------------------------
// Example
// ----------------------------------------------

const Example = Type.Object({
  id: Type.String(),
  testingOptions: Type.Object({
    isTest: Type.Boolean(),
  }),
});

const DeepPartialExample = DeepPartial(Example)

type DeepPartialExample = Static<typeof DeepPartialExample>

Hope this helps S

isaac-scarrott commented 1 year ago

@sinclairzx81 Hi and thanks for the quick and detailed response!

Yep that seems like a good workaround for now and looking forward to the Mapped Type function.

Thanks again

isaac-scarrott commented 1 year ago

Hi, I've implemented this however I'm getting an error when using in Type.Intersect as follows:

const Example = Type.Object({
  id: Type.String(),
  testingOptions: Type.Object({
    isTest: Type.Boolean(),
  }),
});

const UpdateExampleSchema = Type.Intersect([
  DeepPartial(Example),
  Type.Object({ lastModified: TypeDate }),
]);

// error "...is missing the following properties from type 'TObject<TProperties>': type, propertiests"

Wondering if you have a good way to mitigate this @sinclairzx81 as we need lastModified as required, however everything else optional?

sinclairzx81 commented 1 year ago

@isaac-scarrott Try

export type DeepPartial<T extends Record<any, any>> = {
  [K in keyof T]?: T[K] extends Record<any, any> ? DeepPartial<T[K]> : T[K] 
}

// Specialized TObject type that can be passed to TIntersect
export interface TDeepPartial<T extends TObject> extends TObject {
    static: DeepPartial<Static<T>>
}

export function DeepPartial<T extends TObject>(schema: T): TDeepPartial<T> {
  const properties = Object.keys(schema.properties).reduce((acc, key) => {
    const property = schema.properties[key]
    const mapped = (property.type === 'object') ? DeepPartial(property as TObject) : property
    return { ...acc, [key]: Type.Optional(mapped)}
  }, {}) as TProperties
  return Type.Object({ ...properties }) as TDeepPartial<T> // required
}
isaac-scarrott commented 1 year ago

Yep perfect, thanks @sinclairzx81 !