Skip to content

Commit

Permalink
fix: circular ref when optional
Browse files Browse the repository at this point in the history
  • Loading branch information
LuciNyan committed Mar 2, 2023
1 parent c4a1e85 commit fea3d2d
Show file tree
Hide file tree
Showing 4 changed files with 24 additions and 15 deletions.
4 changes: 2 additions & 2 deletions src/operator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ describe('objectOf', () => {
it('should return a function that checks if an object with circular reference is of correct type', () => {
const guard = objectOf({
name: isString,
ref: SELF,
ref: optional(SELF),
age: optional(isNumber),
})

const elem: any = { name: 'element' }

elem.abc = elem
expect(guard(elem)).toBe(false)
expect(guard(elem)).toBe(true)

elem.ref = false
expect(guard(elem)).toBe(false)
Expand Down
9 changes: 6 additions & 3 deletions src/operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,17 @@ export function intersection<T extends any[]>(
}

export function optional<T>(guard: Guard<T>): Guard<T> & {
isOptional: true
__isOptional: true
} {
const _guard = (...args: Parameters<Guard<T>>) => guard(...args)

_guard.isOptional = true
_guard.__isOptional = true
if (guard.__isSelf) {
_guard.__isSelf = guard.__isSelf
}

return _guard as Guard<T> & {
isOptional: true
__isOptional: true
}
}

Expand Down
18 changes: 12 additions & 6 deletions src/property.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { isObject } from './base'
import { Guard, GuardConfig, MakeTypeFromConfig } from './utils'

export const SELF: Guard<any> = (x: unknown): x is any => true
SELF.isSelf = true
const circularRefPlaceholder: Guard<any> = (x: unknown): x is any => true
circularRefPlaceholder.__isSelf = true

export const SELF = circularRefPlaceholder

export function hasUnknownProperty<K extends string>(x: unknown, name: K): x is { [Key in K]: unknown } {
return isObject(x) && name in x
}

export function hasProperty<K extends string, V>(x: unknown, name: K, guard: Guard<V>): x is { [Key in K]: V } {
if (!hasUnknownProperty(x, name)) {
return !!guard.isOptional
return !!guard.__isOptional
}

const memo = new Set([x])

if (guard === SELF) {
if (isCircularRefPlaceholder(guard)) {
return _hasProperty(x[name], name, SELF, memo)
}

Expand All @@ -38,12 +40,12 @@ export function _hasProperty<K extends string, V, R extends GuardConfig>(
guardByProperty?: R
): x is { [Key in K]: V } {
if (!hasUnknownProperty(x, name)) {
return !!guard.isOptional
return !!guard.__isOptional
}

memo.add(x)

if (guard === SELF && guardByProperty) {
if (isCircularRefPlaceholder(guard) && guardByProperty) {
return memo.has(x[name]) ? true : _hasProperties(x[name], guardByProperty, memo)
}

Expand All @@ -61,3 +63,7 @@ export function _hasProperties<R extends GuardConfig>(
return _hasProperty(x, key, guard, memo, guardByProperty)
})
}

function isCircularRefPlaceholder(x: Guard<unknown>): boolean {
return !!x.__isSelf
}
8 changes: 4 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never

export type Guard<T> = ((x: unknown) => x is T) & {
isOptional?: boolean
isSelf?: boolean
__isOptional?: boolean
__isSelf?: boolean
}

export type GuardFor<T> = T extends Guard<infer R> ? R : never
Expand All @@ -17,7 +17,7 @@ export type MakeTypeFromConfig<T extends Record<PropertyKey, Guard<unknown>>> =
>

type handleMetadataIsOptional<T extends GuardConfig, K extends keyof T> = K extends K
? T[K] extends { isOptional: true }
? T[K] extends { __isOptional: true }
? {
[Key in keyof T as Key extends K ? Key : never]?: T[K]
}
Expand All @@ -27,7 +27,7 @@ type handleMetadataIsOptional<T extends GuardConfig, K extends keyof T> = K exte
: never

type handleMetadataIsSelf<T> = {
[K in keyof T]: T[K] extends { isSelf: true } ? handleMetadataIsSelf<T> : GuardFor<T[K]>
[K in keyof T]: T[K] extends { __isSelf: true } ? handleMetadataIsSelf<T> : GuardFor<T[K]>
}

export function toString(x: unknown): string {
Expand Down

0 comments on commit fea3d2d

Please sign in to comment.