Skip to content

Commit 2e8818e

Browse files
authored
Composite Type Optimization (#492)
1 parent 185eb13 commit 2e8818e

File tree

6 files changed

+137
-54
lines changed

6 files changed

+137
-54
lines changed

hammer.mjs

+7
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,14 @@ export async function benchmark() {
2929
// -------------------------------------------------------------------------------
3030
// Test
3131
// -------------------------------------------------------------------------------
32+
export async function test_typescript() {
33+
for (const version of ['4.9.5', '5.0.4', '5.1.3', '5.1.6', 'next', 'latest']) {
34+
await shell(`npm install typescript@${version} --no-save`)
35+
await test_static()
36+
}
37+
}
3238
export async function test_static() {
39+
await shell(`tsc -v`)
3340
await shell(`tsc -p test/static/tsconfig.json --noEmit --strict`)
3441
}
3542
export async function test_runtime(filter = '') {

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sinclair/typebox",
3-
"version": "0.29.3",
3+
"version": "0.29.4",
44
"description": "JSONSchema Type Builder with Static Type Resolution for TypeScript",
55
"keywords": [
66
"typescript",
@@ -24,10 +24,13 @@
2424
"url": "https://github.com/sinclairzx81/typebox"
2525
},
2626
"scripts": {
27+
"test:typescript": "hammer task test_typescript",
28+
"test:static": "hammer task test_static",
29+
"test:runtime": "hammer task test_runtime",
30+
"test": "hammer task test",
2731
"clean": "hammer task clean",
2832
"format": "hammer task format",
2933
"start": "hammer task start",
30-
"test": "hammer task test",
3134
"benchmark": "hammer task benchmark",
3235
"build": "hammer task build",
3336
"publish": "hammer task publish"

readme.md

+35-23
Original file line numberDiff line numberDiff line change
@@ -335,15 +335,15 @@ The following table lists the Standard TypeBox types. These types are fully comp
335335
│ │ │ } │
336336
│ │ │ │
337337
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
338-
const T = Type.Composite([ │ type I = { │ const T = { │
338+
const T = Type.Composite([ │ type T = { │ const T = { │
339339
│ Type.Object({ │ x: numbertype: 'object', │
340340
x: Type.Number() │ } & { │ required: ['x', 'y'], │
341341
│ }), │ y: numberproperties: { │
342342
│ Type.Object({ │ } │ x: { │
343343
│ y: Type.Number() │ │ type: 'number'
344-
│ }) │ type T = { │ }, │
345-
│ ]) │ [K in keyof I]: I[K]y: { │
346-
│ │ }type: 'number'
344+
│ }) │ │ }, │
345+
│ ]) │ │ y: { │
346+
│ │ │ type: 'number'
347347
│ │ │ } │
348348
│ │ │ } │
349349
│ │ │ } │
@@ -739,12 +739,12 @@ const R = Type.Ref(T) // const R = {
739739
740740
### Recursive Types
741741
742-
Recursive types are supported with `Type.Recursive`
742+
Recursive types are supported with `Type.Recursive`.
743743
744744
```typescript
745-
const Node = Type.Recursive(Node => Type.Object({ // const Node = {
745+
const Node = Type.Recursive(This => Type.Object({ // const Node = {
746746
id: Type.String(), // $id: 'Node',
747-
nodes: Type.Array(Node) // type: 'object',
747+
nodes: Type.Array(This) // type: 'object',
748748
}), { $id: 'Node' }) // properties: {
749749
// id: {
750750
// type: 'string'
@@ -776,38 +776,50 @@ function test(node: Node) {
776776
777777
### Conditional Types
778778
779-
Conditional types are supported with `Type.Extends`, `Type.Exclude` and `Type.Extract`
779+
TypeBox supports conditional types with `Type.Extends`. This type will perform a structural assignment check for the first two parameters and return a `true` or `false` type from the second two parameters. The types `Type.Exclude` and `Type.Extract` are also supported.
780780
781781
```typescript
782782
// TypeScript
783783

784784
type T0 = string extends number ? true : false // type T0 = false
785785

786-
type T1 = Extract<string | number, number> // type T1 = number
786+
type T1 = Extract<(1 | 2 | 3), 1> // type T1 = 1
787787

788-
type T2 = Exclude<string | number, number> // type T2 = string
788+
type T2 = Exclude<(1 | 2 | 3), 1> // type T2 = 2 | 3
789789

790790
// TypeBox
791791

792-
const T0 = Type.Extends(Type.String(), Type.Number(), Type.Literal(true), Type.Literal(false))
793-
794-
const T1 = Type.Extract(Type.Union([Type.String(), Type.Number()]), Type.Number())
795-
796-
const T2 = Type.Exclude(Type.Union([Type.String(), Type.Number()]), Type.Number())
797-
798-
799-
type T0 = Static<typeof T0> // type T0 = false
792+
const T0 = Type.Extends( // const T0: TLiteral<false>
793+
Type.String(),
794+
Type.Number(),
795+
Type.Literal(true),
796+
Type.Literal(false)
797+
)
800798

801-
type T1 = Static<typeof T1> // type T1 = number
799+
const T1 = Type.Extract( // const T1: TLiteral<1>
800+
Type.Union([
801+
Type.Literal(1),
802+
Type.Literal(2),
803+
Type.Literal(3)
804+
]),
805+
Type.Literal(1)
806+
)
802807

803-
type T2 = Static<typeof T2> // type T2 = string
808+
const T2 = Type.Exclude( // const T2: TUnion<[
809+
Type.Union([ // TLiteral<2>,
810+
Type.Literal(1), // TLiteral<3>
811+
Type.Literal(2), // ]>
812+
Type.Literal(3)
813+
]),
814+
Type.Literal(1)
815+
)
804816
```
805817
806818
<a name='types-template-literal'></a>
807819
808820
### Template Literal Types
809821
810-
TypeBox supports Template Literal types using `Type.TemplateLiteral`. These types can be created using a simple template DSL syntax, however more complex template literals can be created by passing an array of literal and union types. The examples below show the template DSL syntax.
822+
TypeBox supports template literal types with `Type.TemplateLiteral`. This type implements an embedded DSL syntax to match the TypeScript template literal syntax. This type can also be composed by passing an array of union and literal types as parameters. The following example shows the DSL syntax.
811823
812824
```typescript
813825
// TypeScript
@@ -853,7 +865,7 @@ const R = Type.Record(T, Type.String()) // const R = {
853865
854866
### Indexed Access Types
855867
856-
TypeBox supports Indexed Access types using `Type.Index`. This feature provides a consistent way to access property types without having to extract them from the underlying schema representation. Indexed accessors are supported for object and tuples, as well as nested union and intersect types.
868+
TypeBox supports indexed access types using `Type.Index`. This type provides a consistent way to access interior property and array element types without having to extract them from the underlying schema representation. Indexed access types are supported for object, array, tuple, union and intersect types.
857869
858870
```typescript
859871
const T = Type.Object({ // const T = {
@@ -888,7 +900,7 @@ const C = Type.Index(T, Type.KeyOf(T)) // const C = {
888900
889901
### Not Types
890902
891-
TypeBox has partial support for the JSON schema `not` keyword with `Type.Not`. This type is synonymous with the concept of a [negated types](https://github.com/microsoft/TypeScript/issues/4196) which are not supported in the TypeScript language. TypeBox does provide partial inference support via the intersection of `T & not U` (where all negated types infer as `unknown`). This can be used in the following context.
903+
TypeBox provides support for the `not` keyword with `Type.Not`. This type is synonymous with [negated types](https://github.com/microsoft/TypeScript/issues/4196) which are not supported in the TypeScript language. Partial inference of this type can be attained via the intersection of `T & not U` (where all Not types infer as `unknown`). This approach can be used to narrow for broader types in the following context.
892904
893905
```typescript
894906
// TypeScript

src/typebox.ts

+22-11
Original file line numberDiff line numberDiff line change
@@ -241,16 +241,20 @@ export type TInstanceType<T extends TConstructor<TSchema[], TSchema>> = T['retur
241241
// TComposite
242242
// --------------------------------------------------------------------------
243243
// prettier-ignore
244-
export type TCompositeReduce<T extends TIntersect<TObject[]>, K extends string[]> = K extends [infer L, ...infer R]
245-
? { [_ in Assert<L, string>]: TIndexType<T, Assert<L, string>> } & TCompositeReduce<T, Assert<R, string[]>>
246-
: {}
244+
export type TCompositeKeys<T extends TObject[]> = T extends [infer L, ...infer R]
245+
? keyof Assert<L, TObject>['properties'] | TCompositeKeys<Assert<R, TObject[]>>
246+
: never
247247
// prettier-ignore
248-
export type TCompositeSelect<T extends TIntersect<TObject[]>> = UnionToTuple<keyof Static<T>> extends infer K
249-
? Evaluate<TCompositeReduce<T, Assert<K, string[]>>>
248+
export type TCompositeIndex<T extends TIntersect<TObject[]>, K extends string[]> = K extends [infer L, ...infer R]
249+
? { [_ in Assert<L, string>]: TIndexType<T, Assert<L, string>> } & TCompositeIndex<T, Assert<R, string[]>>
250250
: {}
251251
// prettier-ignore
252-
export type TComposite<T extends TObject[]> = TIntersect<T> extends infer R
253-
? TObject<TCompositeSelect<Assert<R, TIntersect<TObject[]>>>>
252+
export type TCompositeReduce<T extends TObject[]> = UnionToTuple<TCompositeKeys<T>> extends infer K
253+
? Evaluate<TCompositeIndex<TIntersect<T>, Assert<K, string[]>>>
254+
: {} // ^ indexed via intersection of T
255+
// prettier-ignore
256+
export type TComposite<T extends TObject[]> = TIntersect<T> extends TIntersect
257+
? TObject<TCompositeReduce<T>>
254258
: TObject<{}>
255259
// --------------------------------------------------------------------------
256260
// TConstructor
@@ -640,12 +644,21 @@ export type StringFormatOption =
640644
| 'json-pointer'
641645
| 'relative-json-pointer'
642646
| 'regex'
647+
| ({} & string)
648+
// prettier-ignore
649+
export type StringContentEncodingOption =
650+
| '7bit'
651+
| '8bit'
652+
| 'binary'
653+
| 'quoted-printable'
654+
| 'base64'
655+
| ({} & string)
643656
export interface StringOptions extends SchemaOptions {
644657
minLength?: number
645658
maxLength?: number
646659
pattern?: string
647-
format?: string
648-
contentEncoding?: '7bit' | '8bit' | 'binary' | 'quoted-printable' | 'base64'
660+
format?: StringFormatOption
661+
contentEncoding?: StringContentEncodingOption
649662
contentMediaType?: string
650663
}
651664
export interface TString extends TSchema, StringOptions {
@@ -731,9 +744,7 @@ export interface TTemplateLiteral<T extends TTemplateLiteralKind[] = TTemplateLi
731744
// TTuple
732745
// --------------------------------------------------------------------------
733746
export type TTupleIntoArray<T extends TTuple<TSchema[]>> = T extends TTuple<infer R> ? AssertRest<R> : never
734-
735747
export type TTupleInfer<T extends TSchema[], P extends unknown[]> = T extends [infer L, ...infer R] ? [Static<AssertType<L>, P>, ...TTupleInfer<AssertRest<R>, P>] : []
736-
737748
export interface TTuple<T extends TSchema[] = TSchema[]> extends TSchema {
738749
[Kind]: 'Tuple'
739750
static: TTupleInfer<T, this['params']> // { [K in keyof T]: T[K] extends TSchema ? Static<T[K], this['params']> : T[K] }

test/static/composite.ts

+66-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Expect } from './assert'
2-
import { Type, Static } from '@sinclair/typebox'
2+
import { Type, TObject, TIntersect, TNumber, TBoolean } from '@sinclair/typebox'
33

4+
// ----------------------------------------------------------------------------
45
// Overlapping - Non Varying
6+
// ----------------------------------------------------------------------------
57
{
68
const A = Type.Object({
79
A: Type.Number(),
@@ -15,7 +17,9 @@ import { Type, Static } from '@sinclair/typebox'
1517
A: number
1618
}>()
1719
}
20+
// ----------------------------------------------------------------------------
1821
// Overlapping - Varying
22+
// ----------------------------------------------------------------------------
1923
{
2024
const A = Type.Object({
2125
A: Type.Number(),
@@ -29,7 +33,9 @@ import { Type, Static } from '@sinclair/typebox'
2933
A: never
3034
}>()
3135
}
36+
// ----------------------------------------------------------------------------
3237
// Overlapping Single Optional
38+
// ----------------------------------------------------------------------------
3339
{
3440
const A = Type.Object({
3541
A: Type.Optional(Type.Number()),
@@ -43,26 +49,50 @@ import { Type, Static } from '@sinclair/typebox'
4349
A: number
4450
}>()
4551
}
52+
// ----------------------------------------------------------------------------
4653
// Overlapping All Optional (Deferred)
54+
//
4755
// Note for: https://github.com/sinclairzx81/typebox/issues/419
48-
// Determining if a composite property is optional requires a deep check for all properties gathered during a indexed access
49-
// call. Currently, there isn't a trivial way to perform this check without running into possibly infinite instantiation issues.
50-
// The optional check is only specific to overlapping properties. Singular properties will continue to work as expected. The
51-
// rule is "if all composite properties for a key are optional, then the composite property is optional". Defer this test and
52-
// document as minor breaking change.
56+
// ----------------------------------------------------------------------------
5357
{
54-
// const A = Type.Object({
55-
// A: Type.Optional(Type.Number()),
56-
// })
57-
// const B = Type.Object({
58-
// A: Type.Optional(Type.Number()),
59-
// })
60-
// const T = Type.Composite([A, B])
61-
// Expect(T).ToInfer<{
62-
// A: number | undefined
63-
// }>()
58+
const A = Type.Object({
59+
A: Type.Optional(Type.Number()),
60+
})
61+
const B = Type.Object({
62+
A: Type.Optional(Type.Number()),
63+
})
64+
const T = Type.Composite([A, B])
65+
Expect(T).ToInfer<{
66+
A: number | undefined
67+
}>()
6468
}
69+
{
70+
const A = Type.Object({
71+
A: Type.Optional(Type.Number()),
72+
})
73+
const B = Type.Object({
74+
A: Type.Number(),
75+
})
76+
const T = Type.Composite([A, B])
77+
Expect(T).ToInfer<{
78+
A: number
79+
}>()
80+
}
81+
{
82+
const A = Type.Object({
83+
A: Type.Number(),
84+
})
85+
const B = Type.Object({
86+
A: Type.Number(),
87+
})
88+
const T = Type.Composite([A, B])
89+
Expect(T).ToInfer<{
90+
A: number
91+
}>()
92+
}
93+
// ----------------------------------------------------------------------------
6594
// Distinct Properties
95+
// ----------------------------------------------------------------------------
6696
{
6797
const A = Type.Object({
6898
A: Type.Number(),
@@ -77,3 +107,23 @@ import { Type, Static } from '@sinclair/typebox'
77107
B: number
78108
}>()
79109
}
110+
// ----------------------------------------------------------------------------
111+
// Intersection Quirk
112+
//
113+
// TypeScript has an evaluation quirk for the following case where the first
114+
// type evaluates the sub property as never, but the second evaluates the
115+
// entire type as never. There is probably a reason for this behavior, but
116+
// TypeBox supports the former evaluation.
117+
//
118+
// { x: number } & { x: string } -> { x: number } & { x: string } => { x: never }
119+
// { x: number } & { x: boolean } -> never -> ...
120+
// ----------------------------------------------------------------------------
121+
{
122+
// prettier-ignore
123+
const T: TObject<{
124+
x: TIntersect<[TNumber, TBoolean]>
125+
}> = Type.Composite([
126+
Type.Object({ x: Type.Number() }),
127+
Type.Object({ x: Type.Boolean() })
128+
])
129+
}

0 commit comments

Comments
 (0)