Skip to content

Commit c09e133

Browse files
authored
Version 1.0.51 (#1447)
* Add Required Clone function to Base * ChangeLog * Version
1 parent 7e8ac80 commit c09e133

9 files changed

Lines changed: 286 additions & 50 deletions

File tree

changelog/1.0.0.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
---
44

55
### Version Updates
6+
- [Revision 1.0.51](https://github.com/sinclairzx81/typebox/pull/1447)
7+
- Add Required Clone function to Base
68
- [Revision 1.0.50](https://github.com/sinclairzx81/typebox/pull/1443)
79
- Escape Known Property Keys on Additional Properties Checks
810
- [Revision 1.0.49](https://github.com/sinclairzx81/typebox/pull/1436)

design/website/docs/type/base.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,28 @@ Base class for building extended types.
44

55
## Definition
66

7-
The Base type is a class definition that includes methods which can be overridden. The two primary methods to override are Check and Errors, both of which are invoked by TypeBox validators. By convention, Base type definitions begin with a leading `T` and are paired with a factory function that creates instances of the type. The following example demonstrates how to define a DateType using Base.
7+
The Base type is a class definition that includes methods which can be overridden. The primary methods to override are Check and Errors, which are called by TypeBox validators, and Clone, which is used by the TypeBox compositor to create new instances of Base when composing with other types.
8+
9+
> ⚠️ Important: The Clone method must be overridden; otherwise, a `Clone not implemented` exception may be thrown. This method is used by the type compositor to generate new instances during type composition. Clone is typically required when the Base type is used within computed types such as Partial, Required, Pick, or Omit.
10+
11+
By convention, Base type definitions start with a leading T and are paired with a factory function that creates instances of the type. The following example demonstrates how to define a DateType using Base, implementing the required Check, Errors, and Clone methods.
812

913
```typescript
1014
export class TDateType extends Type.Base<Date> {
15+
// required: Used by validation
1116
public override Check(value: unknown): value is Date {
1217
return value instanceof Date
1318
}
19+
// required: Used by validation
1420
public override Errors(value: unknown): object[] {
1521
return !this.Check(value) ? [{ message: 'not a Date'}] : []
1622
}
23+
// required: Used by type compositor
24+
public override Clone(): TDateType {
25+
return new TDateType()
26+
}
1727
}
18-
28+
// factory
1929
export function DateType(): TDateType {
2030
return new TDateType()
2131
}

docs/docs/type/base.html

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
<h1>Base</h1>
22
<p>Base class for building extended types.</p>
33
<h2>Definition</h2>
4-
<p>The Base type is a class definition that includes methods which can be overridden. The two primary methods to override are Check and Errors, both of which are invoked by TypeBox validators. By convention, Base type definitions begin with a leading <code>T</code> and are paired with a factory function that creates instances of the type. The following example demonstrates how to define a DateType using Base. </p>
4+
<p>The Base type is a class definition that includes methods which can be overridden. The primary methods to override are Check and Errors, which are called by TypeBox validators, and Clone, which is used by the TypeBox compositor to create new instances of Base when composing with other types.</p>
5+
<blockquote>
6+
<p>⚠️ Important: The Clone method must be overridden; otherwise, a <code>Clone not implemented</code> exception may be thrown. This method is used by the type compositor to generate new instances during type composition. Clone is typically required when the Base type is used within computed types such as Partial, Required, Pick, or Omit.</p>
7+
</blockquote>
8+
<p>By convention, Base type definitions start with a leading T and are paired with a factory function that creates instances of the type. The following example demonstrates how to define a DateType using Base, implementing the required Check, Errors, and Clone methods.</p>
59
<pre><code class="language-typescript">export class TDateType extends Type.Base&lt;Date&gt; {
10+
// required: Used by validation
611
public override Check(value: unknown): value is Date {
712
return value instanceof Date
813
}
14+
// required: Used by validation
915
public override Errors(value: unknown): object[] {
1016
return !this.Check(value) ? [{ message: &#39;not a Date&#39;}] : []
1117
}
18+
// required: Used by type compositor
19+
public override Clone(): TDateType {
20+
return new TDateType()
21+
}
1222
}
13-
23+
// factory
1424
export function DateType(): TDateType {
1525
return new TDateType()
1626
}

src/compile/validator.ts

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,53 +28,75 @@ THE SOFTWARE.
2828

2929
// deno-fmt-ignore-file
3030

31+
import { Arguments } from '../system/arguments/index.ts'
3132
import { Environment } from '../system/environment/index.ts'
3233
import { type TLocalizedValidationError } from '../error/index.ts'
3334
import { type StaticDecode, type StaticEncode, type TProperties, type TSchema, Base } from '../type/index.ts'
3435
import { Errors, Clean, Convert, Create, Default, Decode, Encode, HasCodec, Parser } from '../value/index.ts'
3536
import { Build } from '../schema/index.ts'
3637

3738
// ------------------------------------------------------------------
38-
// ValidatorType<...>
39+
// Validator<...>
3940
// ------------------------------------------------------------------
4041
export class Validator<Context extends TProperties = TProperties, Type extends TSchema = TSchema> extends Base<StaticEncode<Type, Context>> {
42+
private readonly context: Context
43+
private readonly type: Type
4144
private readonly isEvaluated: boolean
4245
private readonly hasCodec: boolean
4346
private readonly code: string
4447
private readonly check: (value: unknown) => boolean
45-
constructor(
46-
private readonly context: Context,
47-
private readonly type: Type,
48-
) {
48+
/** Constructs a Validator with the given Context and Type. */
49+
constructor(context: Context, type: Type)
50+
/** Constructs a Validator with the given arguments. */
51+
constructor(context: Context, type: Type, isEvaluated: boolean, hasCodec: boolean, code: string, check: (value: unknown) => boolean)
52+
/** Constructs a Validator. */
53+
constructor(...args: unknown[]) {
4954
super()
50-
const result = Build(context, type).Evaluate()
51-
this.hasCodec = HasCodec(context, type)
52-
this.isEvaluated = result.IsEvaluated
53-
this.code = result.Code
54-
this.check = result.Check as never
55+
const matched: [Context, Type, boolean, boolean, string, (value: unknown) => boolean] | [Context, Type] = Arguments.Match(args, {
56+
6: (context, type, isEvalulated, hasCodec, code, check) => [context, type, isEvalulated, hasCodec, code, check],
57+
2: (context, type) => [context, type]
58+
})
59+
if(matched.length === 6) {
60+
const [context, type, isEvaluated, hasCodec, code, check] = matched
61+
this.context = context
62+
this.type = type
63+
this.isEvaluated = isEvaluated
64+
this.hasCodec = hasCodec
65+
this.code = code
66+
this.check = check
67+
} else {
68+
const [context, type] = matched as [Context, Type]
69+
const result = Build(context, type).Evaluate()
70+
this.hasCodec = HasCodec(context, type)
71+
this.context = context
72+
this.type = type
73+
this.isEvaluated = result.IsEvaluated
74+
this.code = result.Code
75+
this.check = result.Check as never
76+
}
5577
}
5678
// ----------------------------------------------------------------
57-
// Evaluated
79+
// IsEvaluated
5880
// ----------------------------------------------------------------
59-
/** Returns true if this validator is using runtime eval optimizations */
81+
/** Returns true if this validator is using runtime eval optimizations. */
6082
public IsEvaluated(): boolean {
6183
return this.isEvaluated
6284
}
6385
// ----------------------------------------------------------------
64-
// Schema
86+
// Context | Type
6587
// ----------------------------------------------------------------
66-
/** Returns the Context for this validator */
88+
/** Returns the Context for this validator. */
6789
public Context(): Context {
6890
return this.context
6991
}
70-
/** Returns the Type for this validator */
92+
/** Returns the Type for this validator. */
7193
public Type(): Type {
7294
return this.type
7395
}
7496
// ----------------------------------------------------------------
7597
// Code
7698
// ----------------------------------------------------------------
77-
/** Returns the generated code for this validator */
99+
/** Returns the generated code for this validator. */
78100
public Code(): string {
79101
return this.code
80102
}
@@ -106,6 +128,17 @@ export class Validator<Context extends TProperties = TProperties, Type extends T
106128
public override Default(value: unknown): unknown {
107129
return Default(this.context, this.type, value)
108130
}
131+
/** Clones this validator. */
132+
public override Clone(): Validator<Context, Type> {
133+
return new Validator<Context, Type>(
134+
this.context,
135+
this.type,
136+
this.isEvaluated,
137+
this.hasCodec,
138+
this.code,
139+
this.check
140+
)
141+
}
109142
// ----------------------------------------------------------------
110143
// Parse | Decode | Encode
111144
// ----------------------------------------------------------------

src/type/engine/instantiate.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { type TReadonly, IsReadonly, ReadonlyAdd, ReadonlyRemove, TReadonlyAdd,
4444
import { type TSchema, type TSchemaOptions, IsSchema } from '../types/schema.ts'
4545
import { type TArray, Array, IsArray, ArrayOptions } from '../types/array.ts'
4646
import { type TAsyncIterator, AsyncIterator, IsAsyncIterator, AsyncIteratorOptions } from '../types/async-iterator.ts'
47+
import { IsBase } from '../types/base.ts'
4748
import { type TConstructor, Constructor, IsConstructor, ConstructorOptions } from '../types/constructor.ts'
4849
import { type TDeferred, Deferred, IsDeferred } from '../types/deferred.ts'
4950
import { type TFunction, Function as _Function, IsFunction, FunctionOptions } from '../types/function.ts'
@@ -336,12 +337,14 @@ function InstantiateDeferred<Context extends TProperties, State extends TState,
336337
// InstantiateType
337338
// ------------------------------------------------------------------
338339
export type TInstantiateType<Context extends TProperties, State extends TState, Input extends TSchema,
339-
IsImmutable extends boolean = Input extends TImmutable ? true : false,
340-
ModifierState extends [TSchema, ModifierAction, ModifierAction] = TModifierActions<Input,
341-
Input extends TReadonly<Input> ? 'add' : 'none',
342-
Input extends TOptional<Input> ? 'add' : 'none'
343-
>,
344-
Type extends TSchema = ModifierState[0],
340+
Cloned extends TSchema = Input, // symmetry only
341+
Immutable extends boolean = Cloned extends TImmutable ? true : false,
342+
Modifiers extends [TSchema, ModifierAction, ModifierAction] = TModifierActions<Cloned,
343+
Cloned extends TReadonly<Cloned> ? 'add' : 'none',
344+
Cloned extends TOptional<Cloned> ? 'add' : 'none'
345+
>,
346+
347+
Type extends TSchema = Modifiers[0],
345348
Instantiated extends TSchema = (
346349
Type extends TRef<infer Ref extends string> ? TRefInstantiate<Context, State, Ref> :
347350
Type extends TArray<infer Type extends TSchema> ? TArray<TInstantiateType<Context, State, Type>> :
@@ -360,16 +363,20 @@ export type TInstantiateType<Context extends TProperties, State extends TState,
360363
Type extends TUnion<infer Types extends TSchema[]> ? TUnion<TInstantiateTypes<Context, State, Types>> :
361364
Type
362365
),
363-
WithImmutable extends TSchema = IsImmutable extends true ? TImmutable<Instantiated> : Instantiated,
364-
WithModifiers extends TSchema = TApplyReadonly<ModifierState[1], TApplyOptional<ModifierState[2], WithImmutable>>,
366+
WithImmutable extends TSchema = Immutable extends true ? TImmutable<Instantiated> : Instantiated,
367+
WithModifiers extends TSchema = TApplyReadonly<Modifiers[1], TApplyOptional<Modifiers[2], WithImmutable>>,
365368
> = WithModifiers
366369

367370
export function InstantiateType<Context extends TProperties, State extends TState, Type extends TSchema>
368371
(context: Context, state: State, input: Type):
369372
TInstantiateType<Context, State, Type> {
370-
const isImmutable = IsImmutable(input)
371-
const modifierActions = ModifierActions(input, IsReadonly(input) ? 'add' : 'none', IsOptional(input) ? 'add' : 'none')
372-
const type = modifierActions[0]
373+
const cloned = IsBase(input) ? input.Clone() : input
374+
const immutable = IsImmutable(cloned)
375+
const modifiers = ModifierActions(cloned,
376+
IsReadonly(cloned) ? 'add' : 'none',
377+
IsOptional(cloned) ? 'add' : 'none')
378+
379+
const type = modifiers[0]
373380
const instantiated = (
374381
IsRef(type) ? RefInstantiate(context, state, type.$ref) :
375382
IsArray(type) ? Array(InstantiateType(context, state, type.items), ArrayOptions(type)) :
@@ -388,8 +395,8 @@ export function InstantiateType<Context extends TProperties, State extends TStat
388395
IsUnion(type) ? Union(InstantiateTypes(context, state, type.anyOf), UnionOptions(type)) :
389396
type
390397
)
391-
const withImmutable = isImmutable ? Immutable(instantiated) : instantiated
392-
const withModifiers = ApplyReadonly(modifierActions[1], ApplyOptional(modifierActions[2], withImmutable))
398+
const withImmutable = immutable ? Immutable(instantiated) : instantiated
399+
const withModifiers = ApplyReadonly(modifiers[1], ApplyOptional(modifiers[2], withImmutable))
393400
return withModifiers as never
394401
}
395402
// ------------------------------------------------------------------

src/type/types/base.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,27 +40,24 @@ export type StaticBase<Value extends unknown> = Value
4040
// ------------------------------------------------------------------
4141
// Type.Base<...>
4242
// ------------------------------------------------------------------
43+
function BaseProperty<Value>(value: Value): PropertyDescriptor {
44+
return {
45+
enumerable: Settings.Get().enumerableKind,
46+
writable: false,
47+
configurable: false,
48+
value
49+
}
50+
}
4351
/** Base class for creating extension types. */
4452
export class Base<Value extends unknown = unknown> implements TSchema, XGuard<Value> {
4553
public readonly '~kind': 'Base'
4654
public readonly '~guard': XGuardInterface<Value>
4755
constructor() {
48-
const configuration = {
49-
enumerable: Settings.Get().enumerableKind,
50-
writable: false,
51-
configurable: false,
52-
}
53-
globalThis.Object.defineProperty(this, '~kind', {
54-
...configuration,
55-
value: 'Base'
56-
})
57-
globalThis.Object.defineProperty(this, '~guard', {
58-
...configuration,
59-
value: {
60-
check: (value): value is Value => this.Check(value),
61-
errors: (value) => this.Errors(value)
62-
} as XGuardInterface<Value>
63-
})
56+
globalThis.Object.defineProperty(this, '~kind', BaseProperty('Base'))
57+
globalThis.Object.defineProperty(this, '~guard', BaseProperty<XGuardInterface<Value>>({
58+
check: (value): value is Value => this.Check(value),
59+
errors: (value) => this.Errors(value)
60+
}))
6461
}
6562
/** Checks a value or returns false if invalid */
6663
public Check(value: unknown): value is Value {
@@ -86,6 +83,10 @@ export class Base<Value extends unknown = unknown> implements TSchema, XGuard<Va
8683
public Create(): Value {
8784
throw new Error('Create not implemented')
8885
}
86+
/** Clones this type */
87+
public Clone(): Base {
88+
throw Error('Clone not implemented')
89+
}
8990
}
9091
// ------------------------------------------------------------------
9192
// Guard

tasks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Range } from './task/range/index.ts'
88
import { Metrics } from './task/metrics/index.ts'
99
import { Task } from 'tasksmith'
1010

11-
const Version = '1.0.50'
11+
const Version = '1.0.51'
1212

1313
// ------------------------------------------------------------------
1414
// Build

test/typebox/runtime/type/types/base.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,69 @@ Test('Should Base 8', () => {
5454
const T: TStringBase = StringBase()
5555
Assert.IsEqual(T.Errors(1), [{ message: 'expected string' }])
5656
})
57+
// ------------------------------------------------------------------
58+
// Clone - Compositor
59+
// ------------------------------------------------------------------
60+
Test('Should Base 9', () => {
61+
class Foo extends Type.Base {
62+
public override Clone(): Type.Base {
63+
return new Foo()
64+
}
65+
}
66+
const T = new Foo()
67+
// Non-Action
68+
const A = Type.Object({ value: T })
69+
Assert.IsFalse(Type.IsOptional(T))
70+
Assert.IsFalse(Type.IsOptional(A.properties.value))
71+
// Action
72+
const B = Type.Partial(A)
73+
Assert.IsFalse(Type.IsOptional(T))
74+
Assert.IsFalse(Type.IsOptional(A.properties.value))
75+
Assert.IsTrue(Type.IsOptional(B.properties.value))
76+
})
77+
Test('Should Base 10', () => {
78+
class Foo extends Type.Base {}
79+
const T = new Foo()
80+
const A = Type.Object({ value: T })
81+
Assert.IsFalse(Type.IsOptional(T))
82+
Assert.IsFalse(Type.IsOptional(A.properties.value))
83+
Assert.Throws(() => Type.Partial(A)) // action require Clone
84+
})
85+
// ------------------------------------------------------------------
86+
// https://github.com/sinclairzx81/typebox/issues/1444
87+
// ------------------------------------------------------------------
88+
Test('Should Base 11', () => {
89+
class TDateType extends Type.Base<Date> {
90+
public override Clone(): TDateType {
91+
return new TDateType()
92+
}
93+
}
94+
const DateType = () => new TDateType()
95+
const A = Type.Object({
96+
status: Type.String(),
97+
updatedDate: DateType(),
98+
createdDate: DateType()
99+
})
100+
const B = Type.Evaluate(Type.Intersect([A, Type.Object({ count: Type.Number() })]))
101+
const C = Type.Object({ freeSchema: Type.Partial(B) })
102+
const D = Type.Evaluate(Type.Intersect([A, Type.Object({ count: Type.Number() })]))
103+
// A
104+
Assert.IsFalse(Type.IsOptional(A.properties.status))
105+
Assert.IsFalse(Type.IsOptional(A.properties.updatedDate))
106+
Assert.IsFalse(Type.IsOptional(A.properties.createdDate))
107+
// B
108+
Assert.IsFalse(Type.IsOptional(B.properties.status))
109+
Assert.IsFalse(Type.IsOptional(B.properties.updatedDate))
110+
Assert.IsFalse(Type.IsOptional(B.properties.createdDate))
111+
Assert.IsFalse(Type.IsOptional(B.properties.count))
112+
// C
113+
Assert.IsTrue(Type.IsOptional(C.properties.freeSchema.properties.status))
114+
Assert.IsTrue(Type.IsOptional(C.properties.freeSchema.properties.updatedDate))
115+
Assert.IsTrue(Type.IsOptional(C.properties.freeSchema.properties.createdDate))
116+
Assert.IsTrue(Type.IsOptional(C.properties.freeSchema.properties.count))
117+
// D
118+
Assert.IsFalse(Type.IsOptional(D.properties.status))
119+
Assert.IsFalse(Type.IsOptional(D.properties.updatedDate))
120+
Assert.IsFalse(Type.IsOptional(D.properties.createdDate))
121+
Assert.IsFalse(Type.IsOptional(D.properties.count))
122+
})

0 commit comments

Comments
 (0)