Composite Type Optimization (#492)

This commit is contained in:
sinclairzx81
2023-07-06 10:09:47 +09:00
committed by GitHub
parent 185eb13dc9
commit 2e8818e71d
6 changed files with 137 additions and 54 deletions
+7
View File
@@ -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 = '') {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@sinclair/typebox",
"version": "0.29.3",
"version": "0.29.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@sinclair/typebox",
"version": "0.29.3",
"version": "0.29.4",
"license": "MIT",
"devDependencies": {
"@sinclair/hammer": "^0.17.1",
+5 -2
View File
@@ -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",
@@ -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"
+35 -23
View File
@@ -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: number │ type: 'object', │
│ x: Type.Number() │ } & { │ required: ['x', 'y'], │
│ }), │ y: number │ properties: { │
│ Type.Object({ │ } │ x: { │
│ y: Type.Number() │ │ type: 'number' │
│ }) │ type T = { │ }, │
│ ]) │ [K in keyof I]: I[K] │ y: { │
│ │ } │ type: 'number' │
│ }) │ │ }, │
│ ]) │ │ y: { │
│ │ │ type: 'number' │
│ │ │ } │
│ │ │ } │
│ │ │ } │
@@ -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'
@@ -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 T0 = Type.Extends( // const T0: TLiteral<false>
Type.String(),
Type.Number(),
Type.Literal(true),
Type.Literal(false)
)
const T1 = Type.Extract(Type.Union([Type.String(), Type.Number()]), Type.Number())
const T1 = Type.Extract( // const T1: TLiteral<1>
Type.Union([
Type.Literal(1),
Type.Literal(2),
Type.Literal(3)
]),
Type.Literal(1)
)
const T2 = Type.Exclude(Type.Union([Type.String(), Type.Number()]), Type.Number())
type T0 = Static<typeof T0> // type T0 = false
type T1 = Static<typeof T1> // type T1 = number
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
@@ -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 = {
@@ -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
+22 -11
View File
@@ -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 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 TCompositeSelect<T extends TIntersect<TObject[]>> = UnionToTuple<keyof Static<T>> extends infer K
? Evaluate<TCompositeReduce<T, Assert<K, string[]>>>
: {}
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 infer R
? TObject<TCompositeSelect<Assert<R, TIntersect<TObject[]>>>>
export type TComposite<T extends TObject[]> = TIntersect<T> extends TIntersect
? TObject<TCompositeReduce<T>>
: TObject<{}>
// --------------------------------------------------------------------------
// TConstructor
@@ -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 {
@@ -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] }
+66 -16
View File
@@ -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(),
@@ -15,7 +17,9 @@ import { Type, Static } from '@sinclair/typebox'
A: number
}>()
}
// ----------------------------------------------------------------------------
// Overlapping - Varying
// ----------------------------------------------------------------------------
{
const A = Type.Object({
A: Type.Number(),
@@ -29,7 +33,9 @@ import { Type, Static } from '@sinclair/typebox'
A: never
}>()
}
// ----------------------------------------------------------------------------
// Overlapping Single Optional
// ----------------------------------------------------------------------------
{
const A = Type.Object({
A: Type.Optional(Type.Number()),
@@ -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(),
@@ -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() })
])
}