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

Adding closure support for circular types #39

Open
ahdinosaur opened this issue Jun 27, 2020 · 7 comments
Open

Adding closure support for circular types #39

ahdinosaur opened this issue Jun 27, 2020 · 7 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@ahdinosaur
Copy link

ahdinosaur commented Jun 27, 2020

hi 😺

i was getting amongst computed-types for a side project i'm playing with, and then i realized the types i want to validate are circular, e.g. a tree data structure. i was wondering if it might be possible to use computed-types with circular types.

i made a simplified example to show what i mean, and what i have working so far: https://repl.it/talk/share/circular-computed-types/43342

// mod.ts

import Schema, { Type, string, array } from 'https://denoporter.sirjosh.workers.dev/v1/deno.land/x/computed_types/src/index.ts'

// lazy due to circular evaluation
let _NodeSchema: any = null
export const NodeSchema: any = function (...args: Array<any>): any {
  if (_NodeSchema == null) throw new Error('programmer error')
  return _NodeSchema(...args)
}

export type Node = Branch | Leaf

export const BranchSchema = Schema({
  name: string.trim().normalize(),
  nodes: array.of(NodeSchema),
})

export type Branch = Type<typeof BranchSchema>

export const LeafSchema = Schema({
  name: string.trim().normalize()
})

export type Leaf = Type<typeof LeafSchema>

_NodeSchema = Schema.either(BranchSchema, LeafSchema)
import { NodeSchema } from './mod.ts'

const node = NodeSchema({
  name: 'a',
  nodes: [
    {
      name: 'b'
    },
    {
      name: 'c',
      nodes: [
        {
          name: 'd',
          nodes: [
            {
              name: 'e'
            }
          ]
        },
        {
          name: 'f'
        }
      ]
    }
  ]
})

console.log(JSON.stringify(node, null, 2))

i'm able to get the runtime to work with a silly hack, but i'm stuck on getting the types to work.

was wondering, is this something that might be possible to do?

cheers! 💜

@moshest
Copy link
Member

moshest commented Jun 28, 2020

Thanks! It's a nice use-case and we definitely want to support it more in the future.

For now, I managed to solve it by explicitly define the type and create a function validator:

type Node = {
  name: string;
  nodes: Node[];
};

const NodeSchema = (node: Node): Node => {
  return Schema({
    name: string.trim().normalize(),
    nodes: array.of(NodeSchema),
  })(node);
};

I added a test for it here: d77a442, 31c38e6

This NodeSchema is a regular type now so you can even use it on other schemas:

const MainSchema = Schema({
  type: string,
  node: NodeSchema,
});

@moshest moshest added question Further information is requested enhancement New feature or request and removed question Further information is requested labels Jun 28, 2020
@calummoore
Copy link
Contributor

calummoore commented Aug 2, 2020

One way to handle this could be to pass in a function which will be called to evaluated the type:

const NodeSchema =  Schema({
    name: string.trim().normalize(),
    nodes: (self) => array.of(self),
  })

And the function could also help in situations where you have circular types across multiple types:

 const SourceFilterOrSchema = Schema({
  operator: 'or',
  left: () => SourceFilterSchema,
  right: () => SourceFilterSchema,
})

const SourceFilterSchema = Schema.either(
  SourceFilterAndSchema,
  SourceFilterOrSchema,
  SourceFilterComparisonSchema,
)

@calummoore
Copy link
Contributor

calummoore commented Aug 2, 2020

In case it helps, this is the schema I've created. This works, but as you can see in the comments, I'm unable to use the auto-typing for SourceFilterAndOr as typescript complains of a circular reference.

import Schema, {
  Type, string, number, boolean,
} from 'computed-types'

const SourceFilterSchema = (node: SourceFilter): SourceFilter => {
  return Schema.either(
    SourceFilterAndOrSchema,
    SourceFilterComparisonSchema,
  )(node)
}

export const SouceFilterPrimitiveSchema = Schema.either(string, number, boolean)

export const SourceFilterAndOrSchema = Schema({
  operator: Schema.either('and' as const, 'or' as const),
  left: SourceFilterSchema,
  right: SourceFilterSchema,
})

export const SchemaFilterComparisonOperatorsSchema = Schema.either(
  'eq' as const,
  'gte' as const,
  'gt' as const, 
  'lt' as const, 
  'lte' as const,
)

export const SourceFilterComparisonSchema = Schema({
  operator: SchemaFilterComparisonOperatorsSchema,
  value: SouceFilterPrimitiveSchema,
})

export type SourceFilter =  SourceFilterAndOr | SourceFilterComparison
export type SourceFilterAndOr = {
  operator: 'and'|'or'
  left: SourceFilter
  right: SourceFilter
}

// If I try to use this instead of the above, typescript errors with: Type alias 'SourceFilterAndOr' circularly references itself.
// export type SourceFilterAndOr = Type<typeof SourceFilterAndOrSchema>
export type SourceFilterComparison = Type<typeof SourceFilterComparisonSchema>
export type SchemaFilterComparisonOperators = Type<typeof SchemaFilterComparisonOperatorsSchema>

@calummoore
Copy link
Contributor

Also, with this approach I'm unable to mark the schema as optional.

const SourceFilterSchema = (node: SourceFilter): SourceFilter => {
  return Schema.either(
    SourceFilterAndOrSchema,
    SourceFilterComparisonSchema,
  )(node)
}

const AltSchema = Schema({
  filter: SourceFilterSchema.optional() // this does not work
})

@moshest
Copy link
Member

moshest commented Aug 2, 2020

I think recursive functions will break TypeScript validation and require type definition from the user, but I really like this idea!

We can create Schema.recursive helper for that:

const Node = Schema.recursive<T>((self: T) => ({
  name: string,
  children: array.of(self),
}));

I will play with it when I have some time.

@calummoore
Copy link
Contributor

calummoore commented Aug 3, 2020

That looks good! I think your approach makes sense for a recursive fn.

The other use case is to enable SchemaA to reference SchemaB and vice versa (as below). In that case, we don't actually need the self variable - we're just using the fn to make sure we have a reference to later defined schema variable.

const SchemaA = Schema({ 
  b: SchemaB // ERROR: SchemaB is not defined
})

const SchemaB = Schema({
  a: SchemaA
})

To solve the above, you could again use the .recursive() utility (although you wouldn't need the self parameter).

const SchemaA = Schema.recursive(() => Schema({ 
  b: SchemaB // Now it works!
}))

const SchemaB = Schema({
  a: SchemaA
})

I wonder if the recursive() name makes sense for this use case though - perhaps the name wrap() would fit both use cases better? Just minor semantics though.

The other difference between recursive and wrap might be that .recursive() works as a replacement to Schema({}) (so you return a plain object) but .wrap() would wrap any Schema (e.g. Schema.merge, Schema.either, etc).

@moshest
Copy link
Member

moshest commented Aug 3, 2020

maybe Schema.closure is a better name then.

I liked your example. Function closure is really a strong concept in JavaScript. I wonder how TypeScript will handle that.

@moshest moshest changed the title circular types Adding closure support for circular types Aug 3, 2020
@moshest moshest added the help wanted Extra attention is needed label Oct 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

3 participants