Skip to content

Commit

Permalink
Composite Type Optimization (#492)
Browse files Browse the repository at this point in the history
  • Loading branch information
sinclairzx81 authored Jul 6, 2023
1 parent 185eb13 commit 2e8818e
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 54 deletions.
7 changes: 7 additions & 0 deletions hammer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ export async function benchmark() {
// -------------------------------------------------------------------------------
// Test
// -------------------------------------------------------------------------------
export async function test_typescript() {
for (const version of ['4.9.5', '5.0.4', '5.1.3', '5.1.6', 'next', 'latest']) {
await shell(`npm install typescript@${version} --no-save`)
await test_static()
}
}
export async function test_static() {
await shell(`tsc -v`)
await shell(`tsc -p test/static/tsconfig.json --noEmit --strict`)
}
export async function test_runtime(filter = '') {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sinclair/typebox",
"version": "0.29.3",
"version": "0.29.4",
"description": "JSONSchema Type Builder with Static Type Resolution for TypeScript",
"keywords": [
"typescript",
Expand All @@ -24,10 +24,13 @@
"url": "https://github.com/sinclairzx81/typebox"
},
"scripts": {
"test:typescript": "hammer task test_typescript",
"test:static": "hammer task test_static",
"test:runtime": "hammer task test_runtime",
"test": "hammer task test",
"clean": "hammer task clean",
"format": "hammer task format",
"start": "hammer task start",
"test": "hammer task test",
"benchmark": "hammer task benchmark",
"build": "hammer task build",
"publish": "hammer task publish"
Expand Down
58 changes: 35 additions & 23 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,15 +335,15 @@ The following table lists the Standard TypeBox types. These types are fully comp
│ │ │ } │
│ │ │ │
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Composite([ │ type I = { │ const T = { │
const T = Type.Composite([ │ type T = { │ const T = { │
│ Type.Object({ │ x: numbertype: 'object', │
x: Type.Number() │ } & { │ required: ['x', 'y'], │
│ }), │ y: numberproperties: { │
│ Type.Object({ │ } │ x: { │
│ y: Type.Number() │ │ type: 'number'
│ }) │ type T = { │ }, │
│ ]) │ [K in keyof I]: I[K]y: { │
│ │ }type: 'number'
│ }) │ │ }, │
│ ]) │ │ y: { │
│ │ │ type: 'number'
│ │ │ } │
│ │ │ } │
│ │ │ } │
Expand Down Expand Up @@ -739,12 +739,12 @@ const R = Type.Ref(T) // const R = {
### Recursive Types
Recursive types are supported with `Type.Recursive`
Recursive types are supported with `Type.Recursive`.
```typescript
const Node = Type.Recursive(Node => Type.Object({ // const Node = {
const Node = Type.Recursive(This => Type.Object({ // const Node = {
id: Type.String(), // $id: 'Node',
nodes: Type.Array(Node) // type: 'object',
nodes: Type.Array(This) // type: 'object',
}), { $id: 'Node' }) // properties: {
// id: {
// type: 'string'
Expand Down Expand Up @@ -776,38 +776,50 @@ function test(node: Node) {
### Conditional Types
Conditional types are supported with `Type.Extends`, `Type.Exclude` and `Type.Extract`
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.
```typescript
// TypeScript

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

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

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

// TypeBox

const T0 = Type.Extends(Type.String(), Type.Number(), Type.Literal(true), Type.Literal(false))

const T1 = Type.Extract(Type.Union([Type.String(), Type.Number()]), Type.Number())

const T2 = Type.Exclude(Type.Union([Type.String(), Type.Number()]), Type.Number())


type T0 = Static<typeof T0> // type T0 = false
const T0 = Type.Extends( // const T0: TLiteral<false>
Type.String(),
Type.Number(),
Type.Literal(true),
Type.Literal(false)
)

type T1 = Static<typeof T1> // type T1 = number
const T1 = Type.Extract( // const T1: TLiteral<1>
Type.Union([
Type.Literal(1),
Type.Literal(2),
Type.Literal(3)
]),
Type.Literal(1)
)

type T2 = Static<typeof T2> // type T2 = string
const T2 = Type.Exclude( // const T2: TUnion<[
Type.Union([ // TLiteral<2>,
Type.Literal(1), // TLiteral<3>
Type.Literal(2), // ]>
Type.Literal(3)
]),
Type.Literal(1)
)
```
<a name='types-template-literal'></a>
### Template Literal Types
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.
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.
```typescript
// TypeScript
Expand Down Expand Up @@ -853,7 +865,7 @@ const R = Type.Record(T, Type.String()) // const R = {
### Indexed Access Types
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.
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.
```typescript
const T = Type.Object({ // const T = {
Expand Down Expand Up @@ -888,7 +900,7 @@ const C = Type.Index(T, Type.KeyOf(T)) // const C = {
### Not Types
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.
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.
```typescript
// TypeScript
Expand Down
33 changes: 22 additions & 11 deletions src/typebox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,16 +241,20 @@ export type TInstanceType<T extends TConstructor<TSchema[], TSchema>> = T['retur
// TComposite
// --------------------------------------------------------------------------
// prettier-ignore
export type TCompositeReduce<T extends TIntersect<TObject[]>, K extends string[]> = K extends [infer L, ...infer R]
? { [_ in Assert<L, string>]: TIndexType<T, Assert<L, string>> } & TCompositeReduce<T, Assert<R, string[]>>
: {}
export type TCompositeKeys<T extends TObject[]> = T extends [infer L, ...infer R]
? keyof Assert<L, TObject>['properties'] | TCompositeKeys<Assert<R, TObject[]>>
: never
// prettier-ignore
export type TCompositeSelect<T extends TIntersect<TObject[]>> = UnionToTuple<keyof Static<T>> extends infer K
? Evaluate<TCompositeReduce<T, Assert<K, string[]>>>
export type TCompositeIndex<T extends TIntersect<TObject[]>, K extends string[]> = K extends [infer L, ...infer R]
? { [_ in Assert<L, string>]: TIndexType<T, Assert<L, string>> } & TCompositeIndex<T, Assert<R, string[]>>
: {}
// prettier-ignore
export type TComposite<T extends TObject[]> = TIntersect<T> extends infer R
? TObject<TCompositeSelect<Assert<R, TIntersect<TObject[]>>>>
export type TCompositeReduce<T extends TObject[]> = UnionToTuple<TCompositeKeys<T>> extends infer K
? Evaluate<TCompositeIndex<TIntersect<T>, Assert<K, string[]>>>
: {} // ^ indexed via intersection of T
// prettier-ignore
export type TComposite<T extends TObject[]> = TIntersect<T> extends TIntersect
? TObject<TCompositeReduce<T>>
: TObject<{}>
// --------------------------------------------------------------------------
// TConstructor
Expand Down Expand Up @@ -640,12 +644,21 @@ export type StringFormatOption =
| 'json-pointer'
| 'relative-json-pointer'
| 'regex'
| ({} & string)
// prettier-ignore
export type StringContentEncodingOption =
| '7bit'
| '8bit'
| 'binary'
| 'quoted-printable'
| 'base64'
| ({} & string)
export interface StringOptions extends SchemaOptions {
minLength?: number
maxLength?: number
pattern?: string
format?: string
contentEncoding?: '7bit' | '8bit' | 'binary' | 'quoted-printable' | 'base64'
format?: StringFormatOption
contentEncoding?: StringContentEncodingOption
contentMediaType?: string
}
export interface TString extends TSchema, StringOptions {
Expand Down Expand Up @@ -731,9 +744,7 @@ export interface TTemplateLiteral<T extends TTemplateLiteralKind[] = TTemplateLi
// TTuple
// --------------------------------------------------------------------------
export type TTupleIntoArray<T extends TTuple<TSchema[]>> = T extends TTuple<infer R> ? AssertRest<R> : never

export type TTupleInfer<T extends TSchema[], P extends unknown[]> = T extends [infer L, ...infer R] ? [Static<AssertType<L>, P>, ...TTupleInfer<AssertRest<R>, P>] : []

export interface TTuple<T extends TSchema[] = TSchema[]> extends TSchema {
[Kind]: 'Tuple'
static: TTupleInfer<T, this['params']> // { [K in keyof T]: T[K] extends TSchema ? Static<T[K], this['params']> : T[K] }
Expand Down
82 changes: 66 additions & 16 deletions test/static/composite.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Expect } from './assert'
import { Type, Static } from '@sinclair/typebox'
import { Type, TObject, TIntersect, TNumber, TBoolean } from '@sinclair/typebox'

// ----------------------------------------------------------------------------
// Overlapping - Non Varying
// ----------------------------------------------------------------------------
{
const A = Type.Object({
A: Type.Number(),
Expand All @@ -15,7 +17,9 @@ import { Type, Static } from '@sinclair/typebox'
A: number
}>()
}
// ----------------------------------------------------------------------------
// Overlapping - Varying
// ----------------------------------------------------------------------------
{
const A = Type.Object({
A: Type.Number(),
Expand All @@ -29,7 +33,9 @@ import { Type, Static } from '@sinclair/typebox'
A: never
}>()
}
// ----------------------------------------------------------------------------
// Overlapping Single Optional
// ----------------------------------------------------------------------------
{
const A = Type.Object({
A: Type.Optional(Type.Number()),
Expand All @@ -43,26 +49,50 @@ import { Type, Static } from '@sinclair/typebox'
A: number
}>()
}
// ----------------------------------------------------------------------------
// Overlapping All Optional (Deferred)
//
// Note for: https://github.com/sinclairzx81/typebox/issues/419
// Determining if a composite property is optional requires a deep check for all properties gathered during a indexed access
// call. Currently, there isn't a trivial way to perform this check without running into possibly infinite instantiation issues.
// The optional check is only specific to overlapping properties. Singular properties will continue to work as expected. The
// rule is "if all composite properties for a key are optional, then the composite property is optional". Defer this test and
// document as minor breaking change.
// ----------------------------------------------------------------------------
{
// const A = Type.Object({
// A: Type.Optional(Type.Number()),
// })
// const B = Type.Object({
// A: Type.Optional(Type.Number()),
// })
// const T = Type.Composite([A, B])
// Expect(T).ToInfer<{
// A: number | undefined
// }>()
const A = Type.Object({
A: Type.Optional(Type.Number()),
})
const B = Type.Object({
A: Type.Optional(Type.Number()),
})
const T = Type.Composite([A, B])
Expect(T).ToInfer<{
A: number | undefined
}>()
}
{
const A = Type.Object({
A: Type.Optional(Type.Number()),
})
const B = Type.Object({
A: Type.Number(),
})
const T = Type.Composite([A, B])
Expect(T).ToInfer<{
A: number
}>()
}
{
const A = Type.Object({
A: Type.Number(),
})
const B = Type.Object({
A: Type.Number(),
})
const T = Type.Composite([A, B])
Expect(T).ToInfer<{
A: number
}>()
}
// ----------------------------------------------------------------------------
// Distinct Properties
// ----------------------------------------------------------------------------
{
const A = Type.Object({
A: Type.Number(),
Expand All @@ -77,3 +107,23 @@ import { Type, Static } from '@sinclair/typebox'
B: number
}>()
}
// ----------------------------------------------------------------------------
// Intersection Quirk
//
// TypeScript has an evaluation quirk for the following case where the first
// type evaluates the sub property as never, but the second evaluates the
// entire type as never. There is probably a reason for this behavior, but
// TypeBox supports the former evaluation.
//
// { x: number } & { x: string } -> { x: number } & { x: string } => { x: never }
// { x: number } & { x: boolean } -> never -> ...
// ----------------------------------------------------------------------------
{
// prettier-ignore
const T: TObject<{
x: TIntersect<[TNumber, TBoolean]>
}> = Type.Composite([
Type.Object({ x: Type.Number() }),
Type.Object({ x: Type.Boolean() })
])
}

0 comments on commit 2e8818e

Please sign in to comment.