Skip to content

Commit 7a7eca7

Browse files
committed
Inference Stack
1 parent d34981b commit 7a7eca7

3 files changed

Lines changed: 165 additions & 36 deletions

File tree

example/index.ts

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,46 @@
11
import Type, { type Static } from 'typebox'
2+
import { Compile } from 'typebox/compile'
3+
import Format from 'typebox/format'
24
import Value from 'typebox/value'
35

4-
// type JsonValue = ( // Type alias 'JsonValue' circularly references itself.
5-
// | JsonValue[]
6-
// | Record<string, JsonValue> // fix: Use Record<string, unknown>
7-
// | string
8-
// | number
9-
// | boolean
10-
// | null
11-
// )
12-
13-
14-
type A = Static<typeof JsonObject>
15-
16-
const { JsonObject, JsonValue, JsonArray } = Type.Module({
17-
JsonArray: Type.Array(Type.Ref('JsonValue')),
18-
JsonObject: Type.Record(Type.String(), Type.Ref('JsonValue')),
19-
JsonValue: Type.Union([
20-
Type.Ref('JsonObject'),
21-
Type.Ref('JsonArray'),
22-
Type.String(),
23-
Type.Number(),
24-
Type.Boolean(),
25-
Type.Null(),
26-
])
6+
// ------------------------------------------------------------------
7+
// Type
8+
// ------------------------------------------------------------------
9+
const T = Type.Object({
10+
x: Type.Number(),
11+
y: Type.Number(),
12+
z: Type.Number()
2713
})
2814

15+
// ------------------------------------------------------------------
16+
// Script
17+
// ------------------------------------------------------------------
18+
const S = Type.Script({ T }, `{
19+
[K in keyof T]: T[K] | null
20+
}`)
2921

22+
// ------------------------------------------------------------------
23+
// Infer
24+
// ------------------------------------------------------------------
25+
type T = Static<typeof T>
26+
type S = Static<typeof S>
27+
28+
// ------------------------------------------------------------------
29+
// Parse
30+
// ------------------------------------------------------------------
31+
32+
const R = Value.Parse(T, { x: 1, y: 2, z: 3 })
33+
34+
// ------------------------------------------------------------------
35+
// Compile
36+
// ------------------------------------------------------------------
37+
38+
const C = Compile(S)
39+
40+
const X = C.Parse({ x: 1, y: 2, z: 3 })
41+
42+
// ------------------------------------------------------------------
43+
// Format
44+
// ------------------------------------------------------------------
45+
46+
const E = Format.IsEmail('user@domain.com')

src/type/types/ref.ts

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,62 @@ import { Memory } from '../../system/memory/index.ts'
3232
import { type StaticType, type StaticDirection } from './static.ts'
3333
import { type TSchema, type TSchemaOptions, IsKind } from './schema.ts'
3434
import { type TProperties } from './properties.ts'
35+
import { type TObject } from './object.ts'
36+
import { type TUnknown } from './unknown.ts'
3537

38+
// ------------------------------------------------------------------
39+
//
40+
// CyclicGuard
41+
//
42+
// This Guard checks if a given Ref already exists in the Stack,
43+
// indicating recursion. If recursion is found, it ensures the Stack
44+
// length remains below a safe threshold.
45+
//
46+
// The purpose is to allow recursive types to instantiate up to a
47+
// reasonable depth (for user feedback) while preventing unbounded
48+
// recursion.
49+
//
50+
// Note: This Guard is only needed for non-Object types, since
51+
// TypeScript automatically defers inference for referential
52+
// mapped property types.
53+
//
54+
// ------------------------------------------------------------------
55+
type CyclicStackLength<Stack extends unknown[], MaxLength extends number, Buffer extends unknown[] = []> = (
56+
Stack extends [infer Left, ...infer Right]
57+
? Buffer['length'] extends MaxLength
58+
? false
59+
: CyclicStackLength<Right, MaxLength, [...Buffer, Left]>
60+
: true
61+
)
62+
type CyclicGuard<Stack extends unknown[], Ref extends string> = (
63+
Ref extends Stack[number] ? CyclicStackLength<Stack, 4> : true
64+
)
3665

3766
// ------------------------------------------------------------------
38-
// Static
67+
// StaticRef
68+
//
69+
// The inference Stack is appended only when encountering a Ref. If the
70+
// referenced target is a TObject, the Stack is reset, and TypeScript's
71+
// built-in inference deferral for referential property types applies.
72+
//
73+
// In all other cases, the Ref is pushed onto the Stack and checked
74+
// with CyclicGuard to ensure recursion is terminated based on the
75+
// CyclicGuard heuristic. Terminated recursion defaults to Any as an
76+
// approximation of the expansive type.
77+
//
3978
// ------------------------------------------------------------------
40-
type StaticTerminate<Stack extends unknown[], Ref extends string> = (
41-
Stack['length'] extends 6
42-
? true
43-
: false
79+
type StaticGuardedRef<Stack extends string[], Direction extends StaticDirection, Context extends TProperties, This extends TProperties, Ref extends string, Type extends TSchema> = (
80+
CyclicGuard<Stack, Ref> extends true
81+
? StaticType<[...Stack, Ref], Direction, Context, This, Type>
82+
: any
4483
)
45-
export type StaticRef<Stack extends string[], Direction extends StaticDirection, Context extends TProperties, This extends TProperties, Ref extends string,
46-
Result extends unknown = (
47-
Ref extends keyof Context
48-
? StaticTerminate<Stack, Ref> extends false
49-
? StaticType<[...Stack, Ref], Direction, Context, This, Context[Ref]>
50-
: any
51-
: unknown
52-
)
84+
export type StaticRef<Stack extends string[], Direction extends StaticDirection, Context extends TProperties, This extends TProperties, Ref extends string,
85+
Target extends TSchema = Ref extends keyof Context ? Context[Ref] : TUnknown,
86+
Result extends unknown = Target extends TObject
87+
? StaticType<[/* Reset */], Direction, Context, This, Target>
88+
: StaticGuardedRef<Stack, Direction, Context, This, Ref, Target>
5389
> = Result
90+
5491
// ------------------------------------------------------------------
5592
// Type
5693
// ------------------------------------------------------------------

test/typebox/static/type/cyclic.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
2+
import { type Static, Type } from 'typebox'
3+
import { Assert } from 'test'
4+
5+
// ------------------------------------------------------------------
6+
// NonCyclic
7+
// ------------------------------------------------------------------
8+
{
9+
const NonCyclic = Type.Cyclic({
10+
NonCyclic: Type.Object({
11+
x: Type.Number(),
12+
y: Type.Number(),
13+
z: Type.Number()
14+
})
15+
}, 'NonCyclic')
16+
type NonCyclic = Static<typeof NonCyclic>
17+
// Invariant
18+
Assert.IsExtendsMutual<{ x: 1, y: 2, z: false }, NonCyclic>(false)
19+
// Extends
20+
Assert.IsExtendsMutual<{ x: number, y: number, z: number }, NonCyclic>(true)
21+
}
22+
23+
// ------------------------------------------------------------------
24+
// Deep
25+
//
26+
// https://github.com/sinclairzx81/typebox/issues/1356
27+
// ------------------------------------------------------------------
28+
{
29+
const Deep = Type.Cyclic({
30+
Deep: Type.Object({
31+
deep: Type.Ref('Deep')
32+
})
33+
}, 'Deep')
34+
type Deep = Static<typeof Deep>
35+
// Invariant
36+
Assert.IsExtends<{ deep: 1 }, Deep>(false)
37+
Assert.IsExtends<{ deep: { deep: { deep: 1 } } }, Deep>(false)
38+
39+
// Extends
40+
Assert.IsExtends<{ deep: any }, Deep>(true)
41+
Assert.IsExtends<{ deep: { deep: { deep: any } } }, Deep>(true)
42+
}
43+
// ------------------------------------------------------------------
44+
// JsonValue
45+
//
46+
// https://github.com/sinclairzx81/typebox/issues/1356
47+
// ------------------------------------------------------------------
48+
{
49+
const JsonValue = Type.Cyclic({
50+
JsonValue: Type.Union([
51+
Type.Record(Type.String(), Type.Ref('JsonValue')),
52+
Type.Array(Type.Ref('JsonValue')),
53+
Type.String(),
54+
Type.Number(),
55+
Type.Boolean(),
56+
Type.Null(),
57+
])
58+
}, 'JsonValue')
59+
60+
type JsonValue = Static<typeof JsonValue>
61+
// Invariant
62+
Assert.IsExtends<bigint, JsonValue>(false)
63+
Assert.IsExtends<bigint[], JsonValue>(false)
64+
Assert.IsExtends<[bigint], JsonValue>(false)
65+
Assert.IsExtends<{ x: bigint }, JsonValue>(false)
66+
67+
// Extends
68+
Assert.IsExtends<'A', JsonValue>(true)
69+
Assert.IsExtends<1, JsonValue>(true)
70+
Assert.IsExtends<true, JsonValue>(true)
71+
Assert.IsExtends<null, JsonValue>(true)
72+
Assert.IsExtends<null[], JsonValue>(true)
73+
Assert.IsExtends<[null], JsonValue>(true)
74+
Assert.IsExtends<{ x: null }, JsonValue>(true)
75+
}

0 commit comments

Comments
 (0)