Skip to content

Commit

Permalink
Support Deep Referential Transform Inference Inside Modules
Browse files Browse the repository at this point in the history
  • Loading branch information
sinclairzx81 committed Feb 20, 2025
1 parent 9cd581a commit 73528b5
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 127 deletions.
208 changes: 125 additions & 83 deletions src/type/module/compute.ts

Large diffs are not rendered by default.

88 changes: 57 additions & 31 deletions src/type/static/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import type { TConstructor } from '../constructor/index'
import type { TEnum } from '../enum/index'
import type { TFunction } from '../function/index'
import type { TIntersect } from '../intersect/index'
import type { TImport } from '../module/index'
import type { TIterator } from '../iterator/index'
import type { TNot } from '../not/index'
import type { TObject, TProperties } from '../object/index'
Expand All @@ -47,48 +48,73 @@ import type { TUnion } from '../union/index'
import type { TUnsafe } from '../unsafe/index'
import type { TSchema } from '../schema/index'
import type { TTransform } from '../transform/index'
import type { TNever } from '../never/index'

// ------------------------------------------------------------------
// DecodeType
// Import
// ------------------------------------------------------------------
// prettier-ignore
export type TDecodeProperties<T extends TProperties> = {
[K in keyof T]: TDecodeType<T[K]>
type TDecodeImport<ModuleProperties extends TProperties, Key extends PropertyKey> = (
Key extends keyof ModuleProperties
? TDecodeType<ModuleProperties[Key]> extends infer Type extends TSchema
? Type extends TRef<infer Ref extends string>
? TDecodeImport<ModuleProperties, Ref>
: Type
: TNever
: TNever
)
// ------------------------------------------------------------------
// Properties
// ------------------------------------------------------------------
// prettier-ignore
type TDecodeProperties<Properties extends TProperties> = {
[Key in keyof Properties]: TDecodeType<Properties[Key]>
}
// ------------------------------------------------------------------
// Types
// ------------------------------------------------------------------
// prettier-ignore
export type TDecodeRest<T extends TSchema[], Acc extends TSchema[] = []> =
T extends [infer L extends TSchema, ...infer R extends TSchema[]]
? TDecodeRest<R, [...Acc, TDecodeType<L>]>
: Acc
type TDecodeTypes<Types extends TSchema[], Result extends TSchema[] = []> = (
Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]]
? TDecodeTypes<Right, [...Result, TDecodeType<Left>]>
: Result
)
// ------------------------------------------------------------------
// Types
// ------------------------------------------------------------------
// prettier-ignore
export type TDecodeType<T extends TSchema> = (
T extends TOptional<infer S extends TSchema> ? TOptional<TDecodeType<S>> :
T extends TReadonly<infer S extends TSchema> ? TReadonly<TDecodeType<S>> :
T extends TTransform<infer _, infer R> ? TUnsafe<R> :
T extends TArray<infer S extends TSchema> ? TArray<TDecodeType<S>> :
T extends TAsyncIterator<infer S extends TSchema> ? TAsyncIterator<TDecodeType<S>> :
T extends TConstructor<infer P extends TSchema[], infer R extends TSchema> ? TConstructor<TDecodeRest<P>, TDecodeType<R>> :
T extends TEnum<infer S> ? TEnum<S> : // intercept for union. interior non decodable
T extends TFunction<infer P extends TSchema[], infer R extends TSchema> ? TFunction<TDecodeRest<P>, TDecodeType<R>> :
T extends TIntersect<infer S extends TSchema[]> ? TIntersect<TDecodeRest<S>> :
T extends TIterator<infer S extends TSchema> ? TIterator<TDecodeType<S>> :
T extends TNot<infer S extends TSchema> ? TNot<TDecodeType<S>> :
T extends TObject<infer S> ? TObject<Evaluate<TDecodeProperties<S>>> :
T extends TPromise<infer S extends TSchema> ? TPromise<TDecodeType<S>> :
T extends TRecord<infer K, infer S> ? TRecord<K, TDecodeType<S>> :
T extends TRecursive<infer S extends TSchema> ? TRecursive<TDecodeType<S>> :
T extends TRef<infer S extends string> ? TRef<S> :
T extends TTuple<infer S extends TSchema[]> ? TTuple<TDecodeRest<S>> :
T extends TUnion<infer S extends TSchema[]> ? TUnion<TDecodeRest<S>> :
T
export type TDecodeType<Type extends TSchema> = (
Type extends TOptional<infer Type extends TSchema> ? TOptional<TDecodeType<Type>> :
Type extends TReadonly<infer Type extends TSchema> ? TReadonly<TDecodeType<Type>> :
Type extends TTransform<infer _Input extends TSchema, infer Output> ? TUnsafe<Output> :
Type extends TArray<infer Type extends TSchema> ? TArray<TDecodeType<Type>> :
Type extends TAsyncIterator<infer Type extends TSchema> ? TAsyncIterator<TDecodeType<Type>> :
Type extends TConstructor<infer Parameters extends TSchema[], infer InstanceType extends TSchema> ? TConstructor<TDecodeTypes<Parameters>, TDecodeType<InstanceType>> :
Type extends TEnum<infer Values> ? TEnum<Values> : // intercept for union. interior non decodable
Type extends TFunction<infer Parameters extends TSchema[], infer ReturnType extends TSchema> ? TFunction<TDecodeTypes<Parameters>, TDecodeType<ReturnType>> :
Type extends TIntersect<infer Types extends TSchema[]> ? TIntersect<TDecodeTypes<Types>> :
Type extends TImport<infer ModuleProperties extends TProperties, infer Key> ? TDecodeImport<ModuleProperties, Key> :
Type extends TIterator<infer Type extends TSchema> ? TIterator<TDecodeType<Type>> :
Type extends TNot<infer Type extends TSchema> ? TNot<TDecodeType<Type>> :
Type extends TObject<infer Properties extends TProperties> ? TObject<Evaluate<TDecodeProperties<Properties>>> :
Type extends TPromise<infer Type extends TSchema> ? TPromise<TDecodeType<Type>> :
Type extends TRecord<infer Key extends TSchema, infer Value extends TSchema> ? TRecord<Key, TDecodeType<Value>> :
Type extends TRecursive<infer Type extends TSchema> ? TRecursive<TDecodeType<Type>> :
Type extends TRef<infer Ref extends string> ? TRef<Ref> :
Type extends TTuple<infer Types extends TSchema[]> ? TTuple<TDecodeTypes<Types>> :
Type extends TUnion<infer Types extends TSchema[]> ? TUnion<TDecodeTypes<Types>> :
Type
)
// ------------------------------------------------------------------
// Static
// ------------------------------------------------------------------
export type StaticDecodeIsAny<T> = boolean extends (T extends TSchema ? true : false) ? true : false
export type StaticDecodeIsAny<Type> = boolean extends (Type extends TSchema ? true : false) ? true : false
/** Creates an decoded static type from a TypeBox type */
export type StaticDecode<T extends TSchema, P extends unknown[] = []> = StaticDecodeIsAny<T> extends true ? unknown : Static<TDecodeType<T>, P>
// prettier-ignore
export type StaticDecode<Type extends TSchema, Params extends unknown[] = [],
Result = StaticDecodeIsAny<Type> extends true ? unknown : Static<TDecodeType<Type>, Params>
> = Result
/** Creates an encoded static type from a TypeBox type */
export type StaticEncode<T extends TSchema, P extends unknown[] = []> = Static<T, P>
export type StaticEncode<Type extends TSchema, Params extends unknown[] = [], Result = Static<Type, Params>> = Result
/** Creates a static type from a TypeBox type */
export type Static<T extends TSchema, P extends unknown[] = []> = (T & { params: P })['static']
export type Static<Type extends TSchema, Params extends unknown[] = [], Result = (Type & { params: Params })['static']> = Result
9 changes: 4 additions & 5 deletions src/value/transform/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,10 @@ function FromIntersect(schema: TIntersect, references: TSchema[], path: string,
}
// prettier-ignore
function FromImport(schema: TImport, references: TSchema[], path: string, value: unknown): unknown {
const definitions = globalThis.Object.values(schema.$defs) as TSchema[]
const additional = globalThis.Object.values(schema.$defs) as TSchema[]
const target = schema.$defs[schema.$ref] as TSchema
const transform = schema[TransformKind as never]
// Note: we need to re-spec the target as TSchema + [TransformKind]
const transformTarget = { [TransformKind]: transform, ...target } as TSchema
return Visit(transformTarget as never, [...references, ...definitions], path, value)
const result = Visit(target, [...references, ...additional], path, value)
return Default(schema, path, result)
}
function FromNot(schema: TNot, references: TSchema[], path: string, value: any): unknown {
return Default(schema, path, Visit(schema.not, references, path, value))
Expand Down Expand Up @@ -202,6 +200,7 @@ function FromUnion(schema: TUnion, references: TSchema[], path: string, value: a
}
return Default(schema, path, value)
}

// prettier-ignore
function Visit(schema: TSchema, references: TSchema[], path: string, value: any): any {
const references_ = Pushref(schema, references)
Expand Down
8 changes: 3 additions & 5 deletions src/value/transform/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,10 @@ function FromArray(schema: TArray, references: TSchema[], path: string, value: a
}
// prettier-ignore
function FromImport(schema: TImport, references: TSchema[], path: string, value: unknown): unknown {
const definitions = globalThis.Object.values(schema.$defs) as TSchema[]
const additional = globalThis.Object.values(schema.$defs) as TSchema[]
const target = schema.$defs[schema.$ref] as TSchema
const transform = schema[TransformKind as never]
// Note: we need to re-spec the target as TSchema + [TransformKind]
const transformTarget = { [TransformKind]: transform, ...target } as TSchema
return Visit(transformTarget as never, [...references, ...definitions], path, value)
const result = Default(schema, path, value)
return Visit(target, [...references, ...additional], path, result)
}
// prettier-ignore
function FromIntersect(schema: TIntersect, references: TSchema[], path: string, value: any) {
Expand Down
11 changes: 10 additions & 1 deletion src/value/transform/has.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ import type { TConstructor } from '../../type/constructor/index'
import type { TFunction } from '../../type/function/index'
import type { TIntersect } from '../../type/intersect/index'
import type { TIterator } from '../../type/iterator/index'
import type { TImport } from '../../type/module/index'
import type { TNot } from '../../type/not/index'
import type { TObject } from '../../type/object/index'
import type { TObject, TProperties } from '../../type/object/index'
import type { TPromise } from '../../type/promise/index'
import type { TRecord } from '../../type/record/index'
import type { TRef } from '../../type/ref/index'
Expand Down Expand Up @@ -75,6 +76,12 @@ function FromIntersect(schema: TIntersect, references: TSchema[]) {
return IsTransform(schema) || IsTransform(schema.unevaluatedProperties) || schema.allOf.some((schema) => Visit(schema, references))
}
// prettier-ignore
function FromImport(schema: TImport, references: TSchema[]) {
const additional = globalThis.Object.getOwnPropertyNames(schema.$defs).reduce((result, key) => [...result, schema.$defs[key as never]], [] as TSchema[])
const target = schema.$defs[schema.$ref]
return IsTransform(schema) || Visit(target, [...additional, ...references])
}
// prettier-ignore
function FromIterator(schema: TIterator, references: TSchema[]) {
return IsTransform(schema) || Visit(schema.items, references)
}
Expand Down Expand Up @@ -135,6 +142,8 @@ function Visit(schema: TSchema, references: TSchema[]): boolean {
return FromConstructor(schema_, references_)
case 'Function':
return FromFunction(schema_, references_)
case 'Import':
return FromImport(schema_, references_)
case 'Intersect':
return FromIntersect(schema_, references_)
case 'Iterator':
Expand Down
68 changes: 66 additions & 2 deletions test/runtime/value/transform/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,13 @@ describe('value/transform/Import', () => {
const T5 = Type.Transform(Type.Module({ A: Type.Object({ x: Type.String(), y: Type.String() })}).Import('A'))
.Decode((value) => new Map(Object.entries(value)))
.Encode((value) => Object.fromEntries(value.entries()) as any)
it('should decode map', () => {
it('Should decode map', () => {
const R = Encoder.Decode(T5, { x: 'hello', y: 'world' })
Assert.IsInstanceOf(R, Map)
Assert.IsEqual(R.get('x'), 'hello')
Assert.IsEqual(R.get('y'), 'world')
})
it('should encode map', () => {
it('Should encode map', () => {
const R = Encoder.Encode(
T5,
new Map([
Expand All @@ -154,4 +154,68 @@ describe('value/transform/Import', () => {
it('Should throw on map decode', () => {
Assert.Throws(() => Encoder.Decode(T5, {}))
})

// -------------------------------------------------------------
// https://github.com/sinclairzx81/typebox/issues/1178
// -------------------------------------------------------------
// immediate
it('Should transform embedded module codec 1', () => {
const T = Type.Module({
A: Type.Transform(Type.String())
.Decode((value) => parseInt(value))
.Encode((value) => value.toString()),
}).Import('A')

const D = Value.Decode(T, '123')
const E = Value.Encode(T, 123)
Assert.IsEqual(D, 123)
Assert.IsEqual(E, '123')
})
// referential
it('Should transform embedded module codec 2', () => {
const T = Type.Module({
A: Type.Transform(Type.String())
.Decode((value) => parseInt(value))
.Encode((value) => value.toString()),
B: Type.Ref('A'),
}).Import('B')
const D = Value.Decode(T, '123')
const E = Value.Encode(T, 123)
Assert.IsEqual(D, 123)
Assert.IsEqual(E, '123')
})
// deep-referential
it('Should transform embedded module codec 3', () => {
const T = Type.Module({
A: Type.Transform(Type.String())
.Decode((value) => parseInt(value))
.Encode((value) => value.toString()),
B: Type.Ref('A'),
C: Type.Ref('B'),
D: Type.Ref('C'),
E: Type.Ref('D'),
}).Import('E')
const D = Value.Decode(T, '123')
const E = Value.Encode(T, 123)
Assert.IsEqual(D, 123)
Assert.IsEqual(E, '123')
})
// interior-transform referential
it('Should transform embedded module codec 4', () => {
const T = Type.Module({
A: Type.String(),
B: Type.Ref('A'),
C: Type.Ref('B'),
T: Type.Transform(Type.Ref('C'))
.Decode((value) => parseInt(value as string))
.Encode((value) => value.toString()),
X: Type.Ref('T'),
Y: Type.Ref('X'),
Z: Type.Ref('Y')
}).Import('Z')
const D = Value.Decode(T, '123')
const E = Value.Encode(T, 123)
Assert.IsEqual(D, 123)
Assert.IsEqual(E, '123')
})
})
54 changes: 54 additions & 0 deletions test/static/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,57 @@ import { Expect } from './assert'
const x1 = c1.Decode({})
const x2 = Value.Decode({} as any, {})
}
// -------------------------------------------------------------
// https://github.com/sinclairzx81/typebox/issues/1178
// -------------------------------------------------------------
// immediate
{
const T = Type.Module({
A: Type.Transform(Type.String())
.Decode((value) => parseInt(value))
.Encode((value) => value.toString()),
}).Import('A')
Expect(T).ToStaticDecode<number>()
Expect(T).ToStaticEncode<string>()
}
// referential
{
const T = Type.Module({
A: Type.Transform(Type.String())
.Decode((value) => parseInt(value))
.Encode((value) => value.toString()),
B: Type.Ref('A'),
}).Import('B')
Expect(T).ToStaticDecode<number>()
Expect(T).ToStaticEncode<string>()
}
// deep-referential
{
const T = Type.Module({
A: Type.Transform(Type.String())
.Decode((value) => parseInt(value))
.Encode((value) => value.toString()),
B: Type.Ref('A'),
C: Type.Ref('B'),
D: Type.Ref('C'),
E: Type.Ref('D'),
}).Import('E')
Expect(T).ToStaticDecode<number>()
Expect(T).ToStaticEncode<string>()
}
// interior-transform referential
{
const T = Type.Module({
A: Type.String(),
B: Type.Ref('A'),
C: Type.Ref('B'),
T: Type.Transform(Type.Ref('C'))
.Decode((value) => parseInt(value as string))
.Encode((value) => value.toString()),
X: Type.Ref('T'),
Y: Type.Ref('X'),
Z: Type.Ref('Y'),
}).Import('Z')
Expect(T).ToStaticDecode<number>()
Expect(T).ToStaticEncode<string>()
}

0 comments on commit 73528b5

Please sign in to comment.