Skip to content

Commit 5130ee5

Browse files
authored
Revision 0.34.27 (#1182)
* Support Deep Referential Transform Inference Inside Modules * Version * ChangeLog
1 parent 9cd581a commit 5130ee5

File tree

10 files changed

+323
-130
lines changed

10 files changed

+323
-130
lines changed

changelog/0.34.0.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
---
44

55
### Revision Updates
6-
6+
- [Revision 0.34.27](https://github.com/sinclairzx81/typebox/pull/1182)
7+
- [1178](https://github.com/sinclairzx81/typebox/issues/1178) Support Deep Referential Transform Inference Inside Modules
78
- [Revision 0.34.26](https://github.com/sinclairzx81/typebox/pull/1181)
89
- Internal: Use Parser Context Threading for Generic Arguments.
910
- [Revision 0.34.25](https://github.com/sinclairzx81/typebox/pull/1176)

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sinclair/typebox",
3-
"version": "0.34.26",
3+
"version": "0.34.27",
44
"description": "Json Schema Type Builder with Static Type Resolution for TypeScript",
55
"keywords": [
66
"typescript",

src/type/module/compute.ts

+125-83
Large diffs are not rendered by default.

src/type/static/static.ts

+57-31
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import type { TConstructor } from '../constructor/index'
3535
import type { TEnum } from '../enum/index'
3636
import type { TFunction } from '../function/index'
3737
import type { TIntersect } from '../intersect/index'
38+
import type { TImport } from '../module/index'
3839
import type { TIterator } from '../iterator/index'
3940
import type { TNot } from '../not/index'
4041
import type { TObject, TProperties } from '../object/index'
@@ -47,48 +48,73 @@ import type { TUnion } from '../union/index'
4748
import type { TUnsafe } from '../unsafe/index'
4849
import type { TSchema } from '../schema/index'
4950
import type { TTransform } from '../transform/index'
51+
import type { TNever } from '../never/index'
5052

5153
// ------------------------------------------------------------------
52-
// DecodeType
54+
// Import
5355
// ------------------------------------------------------------------
5456
// prettier-ignore
55-
export type TDecodeProperties<T extends TProperties> = {
56-
[K in keyof T]: TDecodeType<T[K]>
57+
type TDecodeImport<ModuleProperties extends TProperties, Key extends PropertyKey> = (
58+
Key extends keyof ModuleProperties
59+
? TDecodeType<ModuleProperties[Key]> extends infer Type extends TSchema
60+
? Type extends TRef<infer Ref extends string>
61+
? TDecodeImport<ModuleProperties, Ref>
62+
: Type
63+
: TNever
64+
: TNever
65+
)
66+
// ------------------------------------------------------------------
67+
// Properties
68+
// ------------------------------------------------------------------
69+
// prettier-ignore
70+
type TDecodeProperties<Properties extends TProperties> = {
71+
[Key in keyof Properties]: TDecodeType<Properties[Key]>
5772
}
73+
// ------------------------------------------------------------------
74+
// Types
75+
// ------------------------------------------------------------------
5876
// prettier-ignore
59-
export type TDecodeRest<T extends TSchema[], Acc extends TSchema[] = []> =
60-
T extends [infer L extends TSchema, ...infer R extends TSchema[]]
61-
? TDecodeRest<R, [...Acc, TDecodeType<L>]>
62-
: Acc
77+
type TDecodeTypes<Types extends TSchema[], Result extends TSchema[] = []> = (
78+
Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]]
79+
? TDecodeTypes<Right, [...Result, TDecodeType<Left>]>
80+
: Result
81+
)
82+
// ------------------------------------------------------------------
83+
// Types
84+
// ------------------------------------------------------------------
6385
// prettier-ignore
64-
export type TDecodeType<T extends TSchema> = (
65-
T extends TOptional<infer S extends TSchema> ? TOptional<TDecodeType<S>> :
66-
T extends TReadonly<infer S extends TSchema> ? TReadonly<TDecodeType<S>> :
67-
T extends TTransform<infer _, infer R> ? TUnsafe<R> :
68-
T extends TArray<infer S extends TSchema> ? TArray<TDecodeType<S>> :
69-
T extends TAsyncIterator<infer S extends TSchema> ? TAsyncIterator<TDecodeType<S>> :
70-
T extends TConstructor<infer P extends TSchema[], infer R extends TSchema> ? TConstructor<TDecodeRest<P>, TDecodeType<R>> :
71-
T extends TEnum<infer S> ? TEnum<S> : // intercept for union. interior non decodable
72-
T extends TFunction<infer P extends TSchema[], infer R extends TSchema> ? TFunction<TDecodeRest<P>, TDecodeType<R>> :
73-
T extends TIntersect<infer S extends TSchema[]> ? TIntersect<TDecodeRest<S>> :
74-
T extends TIterator<infer S extends TSchema> ? TIterator<TDecodeType<S>> :
75-
T extends TNot<infer S extends TSchema> ? TNot<TDecodeType<S>> :
76-
T extends TObject<infer S> ? TObject<Evaluate<TDecodeProperties<S>>> :
77-
T extends TPromise<infer S extends TSchema> ? TPromise<TDecodeType<S>> :
78-
T extends TRecord<infer K, infer S> ? TRecord<K, TDecodeType<S>> :
79-
T extends TRecursive<infer S extends TSchema> ? TRecursive<TDecodeType<S>> :
80-
T extends TRef<infer S extends string> ? TRef<S> :
81-
T extends TTuple<infer S extends TSchema[]> ? TTuple<TDecodeRest<S>> :
82-
T extends TUnion<infer S extends TSchema[]> ? TUnion<TDecodeRest<S>> :
83-
T
86+
export type TDecodeType<Type extends TSchema> = (
87+
Type extends TOptional<infer Type extends TSchema> ? TOptional<TDecodeType<Type>> :
88+
Type extends TReadonly<infer Type extends TSchema> ? TReadonly<TDecodeType<Type>> :
89+
Type extends TTransform<infer _Input extends TSchema, infer Output> ? TUnsafe<Output> :
90+
Type extends TArray<infer Type extends TSchema> ? TArray<TDecodeType<Type>> :
91+
Type extends TAsyncIterator<infer Type extends TSchema> ? TAsyncIterator<TDecodeType<Type>> :
92+
Type extends TConstructor<infer Parameters extends TSchema[], infer InstanceType extends TSchema> ? TConstructor<TDecodeTypes<Parameters>, TDecodeType<InstanceType>> :
93+
Type extends TEnum<infer Values> ? TEnum<Values> : // intercept for union. interior non decodable
94+
Type extends TFunction<infer Parameters extends TSchema[], infer ReturnType extends TSchema> ? TFunction<TDecodeTypes<Parameters>, TDecodeType<ReturnType>> :
95+
Type extends TIntersect<infer Types extends TSchema[]> ? TIntersect<TDecodeTypes<Types>> :
96+
Type extends TImport<infer ModuleProperties extends TProperties, infer Key> ? TDecodeImport<ModuleProperties, Key> :
97+
Type extends TIterator<infer Type extends TSchema> ? TIterator<TDecodeType<Type>> :
98+
Type extends TNot<infer Type extends TSchema> ? TNot<TDecodeType<Type>> :
99+
Type extends TObject<infer Properties extends TProperties> ? TObject<Evaluate<TDecodeProperties<Properties>>> :
100+
Type extends TPromise<infer Type extends TSchema> ? TPromise<TDecodeType<Type>> :
101+
Type extends TRecord<infer Key extends TSchema, infer Value extends TSchema> ? TRecord<Key, TDecodeType<Value>> :
102+
Type extends TRecursive<infer Type extends TSchema> ? TRecursive<TDecodeType<Type>> :
103+
Type extends TRef<infer Ref extends string> ? TRef<Ref> :
104+
Type extends TTuple<infer Types extends TSchema[]> ? TTuple<TDecodeTypes<Types>> :
105+
Type extends TUnion<infer Types extends TSchema[]> ? TUnion<TDecodeTypes<Types>> :
106+
Type
84107
)
85108
// ------------------------------------------------------------------
86109
// Static
87110
// ------------------------------------------------------------------
88-
export type StaticDecodeIsAny<T> = boolean extends (T extends TSchema ? true : false) ? true : false
111+
export type StaticDecodeIsAny<Type> = boolean extends (Type extends TSchema ? true : false) ? true : false
89112
/** Creates an decoded static type from a TypeBox type */
90-
export type StaticDecode<T extends TSchema, P extends unknown[] = []> = StaticDecodeIsAny<T> extends true ? unknown : Static<TDecodeType<T>, P>
113+
// prettier-ignore
114+
export type StaticDecode<Type extends TSchema, Params extends unknown[] = [],
115+
Result = StaticDecodeIsAny<Type> extends true ? unknown : Static<TDecodeType<Type>, Params>
116+
> = Result
91117
/** Creates an encoded static type from a TypeBox type */
92-
export type StaticEncode<T extends TSchema, P extends unknown[] = []> = Static<T, P>
118+
export type StaticEncode<Type extends TSchema, Params extends unknown[] = [], Result = Static<Type, Params>> = Result
93119
/** Creates a static type from a TypeBox type */
94-
export type Static<T extends TSchema, P extends unknown[] = []> = (T & { params: P })['static']
120+
export type Static<Type extends TSchema, Params extends unknown[] = [], Result = (Type & { params: Params })['static']> = Result

src/value/transform/decode.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,10 @@ function FromIntersect(schema: TIntersect, references: TSchema[], path: string,
118118
}
119119
// prettier-ignore
120120
function FromImport(schema: TImport, references: TSchema[], path: string, value: unknown): unknown {
121-
const definitions = globalThis.Object.values(schema.$defs) as TSchema[]
121+
const additional = globalThis.Object.values(schema.$defs) as TSchema[]
122122
const target = schema.$defs[schema.$ref] as TSchema
123-
const transform = schema[TransformKind as never]
124-
// Note: we need to re-spec the target as TSchema + [TransformKind]
125-
const transformTarget = { [TransformKind]: transform, ...target } as TSchema
126-
return Visit(transformTarget as never, [...references, ...definitions], path, value)
123+
const result = Visit(target, [...references, ...additional], path, value)
124+
return Default(schema, path, result)
127125
}
128126
function FromNot(schema: TNot, references: TSchema[], path: string, value: any): unknown {
129127
return Default(schema, path, Visit(schema.not, references, path, value))
@@ -202,6 +200,7 @@ function FromUnion(schema: TUnion, references: TSchema[], path: string, value: a
202200
}
203201
return Default(schema, path, value)
204202
}
203+
205204
// prettier-ignore
206205
function Visit(schema: TSchema, references: TSchema[], path: string, value: any): any {
207206
const references_ = Pushref(schema, references)

src/value/transform/encode.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,10 @@ function FromArray(schema: TArray, references: TSchema[], path: string, value: a
9898
}
9999
// prettier-ignore
100100
function FromImport(schema: TImport, references: TSchema[], path: string, value: unknown): unknown {
101-
const definitions = globalThis.Object.values(schema.$defs) as TSchema[]
101+
const additional = globalThis.Object.values(schema.$defs) as TSchema[]
102102
const target = schema.$defs[schema.$ref] as TSchema
103-
const transform = schema[TransformKind as never]
104-
// Note: we need to re-spec the target as TSchema + [TransformKind]
105-
const transformTarget = { [TransformKind]: transform, ...target } as TSchema
106-
return Visit(transformTarget as never, [...references, ...definitions], path, value)
103+
const result = Default(schema, path, value)
104+
return Visit(target, [...references, ...additional], path, result)
107105
}
108106
// prettier-ignore
109107
function FromIntersect(schema: TIntersect, references: TSchema[], path: string, value: any) {

src/value/transform/has.ts

+9
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import type { TConstructor } from '../../type/constructor/index'
3636
import type { TFunction } from '../../type/function/index'
3737
import type { TIntersect } from '../../type/intersect/index'
3838
import type { TIterator } from '../../type/iterator/index'
39+
import type { TImport } from '../../type/module/index'
3940
import type { TNot } from '../../type/not/index'
4041
import type { TObject } from '../../type/object/index'
4142
import type { TPromise } from '../../type/promise/index'
@@ -75,6 +76,12 @@ function FromIntersect(schema: TIntersect, references: TSchema[]) {
7576
return IsTransform(schema) || IsTransform(schema.unevaluatedProperties) || schema.allOf.some((schema) => Visit(schema, references))
7677
}
7778
// prettier-ignore
79+
function FromImport(schema: TImport, references: TSchema[]) {
80+
const additional = globalThis.Object.getOwnPropertyNames(schema.$defs).reduce((result, key) => [...result, schema.$defs[key as never]], [] as TSchema[])
81+
const target = schema.$defs[schema.$ref]
82+
return IsTransform(schema) || Visit(target, [...additional, ...references])
83+
}
84+
// prettier-ignore
7885
function FromIterator(schema: TIterator, references: TSchema[]) {
7986
return IsTransform(schema) || Visit(schema.items, references)
8087
}
@@ -135,6 +142,8 @@ function Visit(schema: TSchema, references: TSchema[]): boolean {
135142
return FromConstructor(schema_, references_)
136143
case 'Function':
137144
return FromFunction(schema_, references_)
145+
case 'Import':
146+
return FromImport(schema_, references_)
138147
case 'Intersect':
139148
return FromIntersect(schema_, references_)
140149
case 'Iterator':

test/runtime/value/transform/import.ts

+66-2
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,13 @@ describe('value/transform/Import', () => {
135135
const T5 = Type.Transform(Type.Module({ A: Type.Object({ x: Type.String(), y: Type.String() })}).Import('A'))
136136
.Decode((value) => new Map(Object.entries(value)))
137137
.Encode((value) => Object.fromEntries(value.entries()) as any)
138-
it('should decode map', () => {
138+
it('Should decode map', () => {
139139
const R = Encoder.Decode(T5, { x: 'hello', y: 'world' })
140140
Assert.IsInstanceOf(R, Map)
141141
Assert.IsEqual(R.get('x'), 'hello')
142142
Assert.IsEqual(R.get('y'), 'world')
143143
})
144-
it('should encode map', () => {
144+
it('Should encode map', () => {
145145
const R = Encoder.Encode(
146146
T5,
147147
new Map([
@@ -154,4 +154,68 @@ describe('value/transform/Import', () => {
154154
it('Should throw on map decode', () => {
155155
Assert.Throws(() => Encoder.Decode(T5, {}))
156156
})
157+
158+
// -------------------------------------------------------------
159+
// https://github.com/sinclairzx81/typebox/issues/1178
160+
// -------------------------------------------------------------
161+
// immediate
162+
it('Should transform embedded module codec 1', () => {
163+
const T = Type.Module({
164+
A: Type.Transform(Type.String())
165+
.Decode((value) => parseInt(value))
166+
.Encode((value) => value.toString()),
167+
}).Import('A')
168+
169+
const D = Value.Decode(T, '123')
170+
const E = Value.Encode(T, 123)
171+
Assert.IsEqual(D, 123)
172+
Assert.IsEqual(E, '123')
173+
})
174+
// referential
175+
it('Should transform embedded module codec 2', () => {
176+
const T = Type.Module({
177+
A: Type.Transform(Type.String())
178+
.Decode((value) => parseInt(value))
179+
.Encode((value) => value.toString()),
180+
B: Type.Ref('A'),
181+
}).Import('B')
182+
const D = Value.Decode(T, '123')
183+
const E = Value.Encode(T, 123)
184+
Assert.IsEqual(D, 123)
185+
Assert.IsEqual(E, '123')
186+
})
187+
// deep-referential
188+
it('Should transform embedded module codec 3', () => {
189+
const T = Type.Module({
190+
A: Type.Transform(Type.String())
191+
.Decode((value) => parseInt(value))
192+
.Encode((value) => value.toString()),
193+
B: Type.Ref('A'),
194+
C: Type.Ref('B'),
195+
D: Type.Ref('C'),
196+
E: Type.Ref('D'),
197+
}).Import('E')
198+
const D = Value.Decode(T, '123')
199+
const E = Value.Encode(T, 123)
200+
Assert.IsEqual(D, 123)
201+
Assert.IsEqual(E, '123')
202+
})
203+
// interior-transform referential
204+
it('Should transform embedded module codec 4', () => {
205+
const T = Type.Module({
206+
A: Type.String(),
207+
B: Type.Ref('A'),
208+
C: Type.Ref('B'),
209+
T: Type.Transform(Type.Ref('C'))
210+
.Decode((value) => parseInt(value as string))
211+
.Encode((value) => value.toString()),
212+
X: Type.Ref('T'),
213+
Y: Type.Ref('X'),
214+
Z: Type.Ref('Y')
215+
}).Import('Z')
216+
const D = Value.Decode(T, '123')
217+
const E = Value.Encode(T, 123)
218+
Assert.IsEqual(D, 123)
219+
Assert.IsEqual(E, '123')
220+
})
157221
})

test/static/transform.ts

+54
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,57 @@ import { Expect } from './assert'
320320
const x1 = c1.Decode({})
321321
const x2 = Value.Decode({} as any, {})
322322
}
323+
// -------------------------------------------------------------
324+
// https://github.com/sinclairzx81/typebox/issues/1178
325+
// -------------------------------------------------------------
326+
// immediate
327+
{
328+
const T = Type.Module({
329+
A: Type.Transform(Type.String())
330+
.Decode((value) => parseInt(value))
331+
.Encode((value) => value.toString()),
332+
}).Import('A')
333+
Expect(T).ToStaticDecode<number>()
334+
Expect(T).ToStaticEncode<string>()
335+
}
336+
// referential
337+
{
338+
const T = Type.Module({
339+
A: Type.Transform(Type.String())
340+
.Decode((value) => parseInt(value))
341+
.Encode((value) => value.toString()),
342+
B: Type.Ref('A'),
343+
}).Import('B')
344+
Expect(T).ToStaticDecode<number>()
345+
Expect(T).ToStaticEncode<string>()
346+
}
347+
// deep-referential
348+
{
349+
const T = Type.Module({
350+
A: Type.Transform(Type.String())
351+
.Decode((value) => parseInt(value))
352+
.Encode((value) => value.toString()),
353+
B: Type.Ref('A'),
354+
C: Type.Ref('B'),
355+
D: Type.Ref('C'),
356+
E: Type.Ref('D'),
357+
}).Import('E')
358+
Expect(T).ToStaticDecode<number>()
359+
Expect(T).ToStaticEncode<string>()
360+
}
361+
// interior-transform referential
362+
{
363+
const T = Type.Module({
364+
A: Type.String(),
365+
B: Type.Ref('A'),
366+
C: Type.Ref('B'),
367+
T: Type.Transform(Type.Ref('C'))
368+
.Decode((value) => parseInt(value as string))
369+
.Encode((value) => value.toString()),
370+
X: Type.Ref('T'),
371+
Y: Type.Ref('X'),
372+
Z: Type.Ref('Y'),
373+
}).Import('Z')
374+
Expect(T).ToStaticDecode<number>()
375+
Expect(T).ToStaticEncode<string>()
376+
}

0 commit comments

Comments
 (0)