From 0661aca2abf18b4e94913af0555ea6528135f64b Mon Sep 17 00:00:00 2001 From: sinclairzx81 Date: Tue, 25 Apr 2023 18:45:15 +0900 Subject: [PATCH] Enhanced Indexed Access Types (#413) --- package-lock.json | 4 +- package.json | 2 +- src/typebox.ts | 129 +++++++++++------------ test/runtime/type/guard/indexed.ts | 64 ++++++++++++ test/static/indexed.ts | 161 +++++++++++++++++++++++++++++ 5 files changed, 288 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ccf314..43f8a0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.28.5", + "version": "0.28.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.28.5", + "version": "0.28.6", "license": "MIT", "devDependencies": { "@sinclair/hammer": "^0.17.1", diff --git a/package.json b/package.json index a34bb2f..4b01987 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.28.5", + "version": "0.28.6", "description": "JSONSchema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", diff --git a/src/typebox.ts b/src/typebox.ts index 21df623..65feb59 100644 --- a/src/typebox.ts +++ b/src/typebox.ts @@ -49,6 +49,8 @@ export type TupleToUnion = { [K in keyof T]: T[K] }[number] export type UnionToIntersect = (U extends unknown ? (arg: U) => 0 : never) extends (arg: infer I) => 0 ? I : never export type UnionLast = UnionToIntersect 0 : never> extends (x: infer L) => 0 ? L : never export type UnionToTuple> = [U] extends [never] ? [] : [...UnionToTuple>, L] +export type Discard = T extends [infer L, ...infer R] ? (L extends D ? Discard : [L, ...Discard]) : [] +export type Flat = T extends [] ? [] : T extends [infer L] ? [...Flat] : T extends [infer L, ...infer R] ? [...Flat, ...Flat] : [T] export type Assert = T extends E ? T : never export type Evaluate = T extends infer O ? { [K in keyof O]: O[K] } : never export type Ensure = T extends infer U ? U : never @@ -310,50 +312,36 @@ export interface TFunction = T extends [infer L, ...infer R] ? [TIndexType, K>, ...TIndexRest, K>] : [] +export type TIndexProperty = K extends keyof T ? [T[K]] : [] +export type TIndexTuple = K extends keyof T ? [T[K]] : [] // prettier-ignore -export type TIndexProperty = - T[K] extends infer R ? [R] : +export type TIndexType = + T extends TRecursive ? TIndexType : + T extends TIntersect ? IntersectType>, TNever>>> : + T extends TUnion ? UnionType>>> : + T extends TObject ? UnionType>>> : + T extends TTuple ? UnionType>>> : [] // prettier-ignore -export type TIndexTuple = - K extends keyof T ? [T[K]] : - [] +export type TIndexRestMany = + K extends [infer L, ...infer R] ? [TIndexType>, ...TIndexRestMany>] : + [] // prettier-ignore -export type TIndexComposite = - T extends [infer L, ...infer R] ? [...TIndexKey, K>, ...TIndexComposite, K>] : - [] -// prettier-ignore -export type TIndexKey = - T extends TRecursive ? TIndexKey : - T extends TIntersect ? TIndexComposite : - T extends TUnion ? TIndexComposite : - T extends TObject ? TIndexProperty : - T extends TTuple ? TIndexTuple : - T extends TArray ? S : - [] -// prettier-ignore -export type TIndexKeys = - K extends [infer L, ...infer R] ? - [...TIndexKey>, ...TIndexKeys>] : - [] -// prettier-ignore -export type TIndexFromKeyTuple = - TIndexKeys extends infer R ? - T extends TRecursive ? TIndexFromKeyTuple : - T extends TTuple ? UnionType> : - T extends TIntersect ? UnionType> : - T extends TUnion ? UnionType> : - T extends TObject ? UnionType> : - T extends TArray ? UnionType> : - TNever : +export type TIndexReduce = + T extends TRecursive ? TIndexReduce : + T extends TIntersect ? UnionType>> : + T extends TUnion ? UnionType>> : + T extends TObject ? UnionType>> : + T extends TTuple ? UnionType>> : TNever // prettier-ignore export type TIndex = [T, K] extends [TTuple, TNumber] ? UnionType> : [T, K] extends [TArray, TNumber] ? AssertType : - K extends TTemplateLiteral ? TIndexFromKeyTuple> : - K extends TUnion[]> ? TIndexFromKeyTuple> : - K extends TLiteral ? TIndexFromKeyTuple : + K extends TTemplateLiteral ? TIndexReduce> : + K extends TUnion[]> ? TIndexReduce> : + K extends TLiteral ? TIndexReduce : TNever // -------------------------------------------------------------------------- // TInteger @@ -379,12 +367,9 @@ export interface TIntersect extends TSchema, In // -------------------------------------------------------------------------- // TKeyOf // -------------------------------------------------------------------------- +// prettier-ignore export type TKeyOfProperties = Static extends infer S - ? UnionToTuple< - { - [K in keyof S]: TLiteral<`${Assert}`> - }[keyof S] - > + ? UnionToTuple<{[K in keyof S]: TLiteral<`${Assert}`>}[keyof S]> : [] // prettier-ignore export type TKeyOfIndicesArray = UnionToTuple @@ -687,7 +672,7 @@ export type TTemplateLiteralConst = export type TTemplateLiteralUnion = T extends [infer L, ...infer R] ? `${TTemplateLiteralConst}${TTemplateLiteralUnion, Acc>}` : Acc -export type TTemplateLiteralKeyTuple = Assert>, Key[]> +export type TTemplateLiteralKeyRest = Assert>, Key[]> export interface TTemplateLiteral extends TSchema { [Kind]: 'TemplateLiteral' static: TTemplateLiteralUnion @@ -727,7 +712,7 @@ export type TLiteralUnionReduce[]> = T extends [infer L, ...infer R] ? [Assert>['const'], ...TLiteralUnionReduce[]>>] : [] // prettier-ignore -export type TUnionLiteral[]>> = +export type TUnionLiteralKeyRest[]>> = T extends TUnion ? TLiteralUnionReduce[]>> : [] // -------------------------------------------------------------------------- @@ -782,9 +767,8 @@ export interface TVoid extends TSchema { // -------------------------------------------------------------------------- // Static // -------------------------------------------------------------------------- -/** Creates a TypeScript static type from a TypeBox type */ +/** Infers a static type from a TypeBox type */ export type Static = (T & { params: P })['static'] - // -------------------------------------------------------------------------- // TypeRegistry // -------------------------------------------------------------------------- @@ -1929,29 +1913,37 @@ export namespace TypeClone { // IndexedAccessor // -------------------------------------------------------------------------- export namespace IndexedAccessor { - function Intersect(schema: TIntersect, key: string): TSchema[] { - return schema.allOf.reduce((acc, schema) => [...acc, ...Visit(schema, key)], [] as TSchema[]) + function Intersect(schema: TIntersect, key: string): TSchema { + const schemas = schema.allOf.reduce((acc, schema) => { + const indexed = Visit(schema, key) + return indexed[Kind] === 'Never' ? acc : [...acc, indexed] + }, [] as TSchema[]) + return Type.Intersect(schemas) } - function Union(schema: TUnion, key: string): TSchema[] { - return schema.anyOf.reduce((acc, schema) => [...acc, ...Visit(schema, key)], [] as TSchema[]) + function Union(schema: TUnion, key: string): TSchema { + const schemas = schema.anyOf.map((schema) => Visit(schema, key)) + return Type.Union(schemas) } - function Object(schema: TObject, key: string): TSchema[] { - const keys = globalThis.Object.getOwnPropertyNames(schema.properties).filter((key_) => key_ === key) - return keys.map((key) => schema.properties[key]) + function Object(schema: TObject, key: string): TSchema { + const property = schema.properties[key] + return property === undefined ? Type.Never() : Type.Union([property]) } - function Tuple(schema: TTuple, key: string): TSchema[] { - const items = schema.items === undefined ? [] : (schema.items as TSchema[]) - return items.filter((_, index) => index.toString() === key) + function Tuple(schema: TTuple, key: string): TSchema { + const items = schema.items + if (items === undefined) return Type.Never() + const element = items[key as any as number] // + if (element === undefined) return Type.Never() + return element } - function Visit(schema: TSchema, key: string): TSchema[] { + function Visit(schema: TSchema, key: string): TSchema { if (schema[Kind] === 'Intersect') return Intersect(schema as TIntersect, key) if (schema[Kind] === 'Union') return Union(schema as TUnion, key) if (schema[Kind] === 'Object') return Object(schema as TObject, key) if (schema[Kind] === 'Tuple') return Tuple(schema as TTuple, key) - return [] + return Type.Never() } - export function Resolve(schema: TSchema, keys: (string | number)[]): TSchema[] { - return keys.reduce((acc, key) => [...acc, ...Visit(schema, key.toString())], [] as TSchema[]) + export function Resolve(schema: TSchema, keys: Key[], options: SchemaOptions = {}): TSchema { + return Type.Union(keys.map((key) => Visit(schema, key.toString()))) } } // -------------------------------------------------------------------------- @@ -2419,23 +2411,22 @@ export class StandardTypeBuilder extends TypeBuilder { } } /** `[Standard]` Returns indexed property types for the given keys */ - public Index)[]>(schema: T, keys: [...K], options?: SchemaOptions): TIndexFromKeyTuple> + public Index)[]>(schema: T, keys: [...K], options?: SchemaOptions): TIndexReduce> /** `[Standard]` Returns indexed property types for the given keys */ public Index(schema: T, key: K, options?: SchemaOptions): TIndex /** `[Standard]` Returns indexed property types for the given keys */ public Index(schema: TSchema, unresolved: any, options: SchemaOptions = {}): any { - const keys = KeyArrayResolver.Resolve(unresolved) if (TypeGuard.TArray(schema) && TypeGuard.TNumber(unresolved)) { return TypeClone.Clone(schema.items, options) - } - if (TypeGuard.TTuple(schema) && TypeGuard.TNumber(unresolved)) { + } else if (TypeGuard.TTuple(schema) && TypeGuard.TNumber(unresolved)) { const items = schema.items === undefined ? [] : schema.items const cloned = items.map((schema) => TypeClone.Clone(schema, {})) return this.Union(cloned, options) + } else { + const keys = KeyArrayResolver.Resolve(unresolved) + const clone = TypeClone.Clone(schema, {}) + return IndexedAccessor.Resolve(clone, keys, options) } - const resolved = IndexedAccessor.Resolve(schema, keys as any) - const cloned = resolved.map((schema) => TypeClone.Clone(schema, {})) - return this.Union(cloned, options) } /** `[Standard]` Creates an Integer type */ public Integer(options: NumericOptions = {}): TInteger { @@ -2515,11 +2506,11 @@ export class StandardTypeBuilder extends TypeBuilder { /** `[Standard]` Creates a mapped type whose keys are omitted from the given type */ public Omit)[]>(schema: T, keys: readonly [...K], options?: SchemaOptions): TOmit /** `[Standard]` Creates a mapped type whose keys are omitted from the given type */ - public Omit[]>>(schema: T, keys: K, options?: SchemaOptions): TOmit[number]> + public Omit[]>>(schema: T, keys: K, options?: SchemaOptions): TOmit[number]> /** `[Standard]` Creates a mapped type whose keys are omitted from the given type */ public Omit>(schema: T, key: K, options?: SchemaOptions): TOmit /** `[Standard]` Creates a mapped type whose keys are omitted from the given type */ - public Omit(schema: T, key: K, options?: SchemaOptions): TOmit[number]> + public Omit(schema: T, key: K, options?: SchemaOptions): TOmit[number]> /** `[Standard]` Creates a mapped type whose keys are omitted from the given type */ public Omit(schema: T, key: K, options?: SchemaOptions): TOmit public Omit(schema: TSchema, unresolved: any, options: SchemaOptions = {}): any { @@ -2557,11 +2548,11 @@ export class StandardTypeBuilder extends TypeBuilder { /** `[Standard]` Creates a mapped type whose keys are picked from the given type */ public Pick)[]>(schema: T, keys: readonly [...K], options?: SchemaOptions): TPick /** `[Standard]` Creates a mapped type whose keys are picked from the given type */ - public Pick[]>>(schema: T, keys: K, options?: SchemaOptions): TPick[number]> + public Pick[]>>(schema: T, keys: K, options?: SchemaOptions): TPick[number]> /** `[Standard]` Creates a mapped type whose keys are picked from the given type */ public Pick>(schema: T, key: K, options?: SchemaOptions): TPick /** `[Standard]` Creates a mapped type whose keys are picked from the given type */ - public Pick(schema: T, key: K, options?: SchemaOptions): TPick[number]> + public Pick(schema: T, key: K, options?: SchemaOptions): TPick[number]> /** `[Standard]` Creates a mapped type whose keys are picked from the given type */ public Pick(schema: T, key: K, options?: SchemaOptions): TPick public Pick(schema: TSchema, unresolved: any, options: SchemaOptions = {}): any { diff --git a/test/runtime/type/guard/indexed.ts b/test/runtime/type/guard/indexed.ts index 84683b5..bc47ba6 100644 --- a/test/runtime/type/guard/indexed.ts +++ b/test/runtime/type/guard/indexed.ts @@ -169,4 +169,68 @@ describe('type/guard/TIndex', () => { const I = Type.Index(T, Type.Object({})) Assert.isTrue(TypeGuard.TNever(I)) }) + it('Should Index 22', () => { + const A = Type.Object({ x: Type.Literal('A') }) + const B = Type.Object({ x: Type.Literal('B') }) + const C = Type.Object({ x: Type.Literal('C') }) + const D = Type.Object({ x: Type.Literal('D') }) + const T = Type.Intersect([A, B, C, D]) + const I = Type.Index(T, ['x']) + Assert.isTrue(TypeGuard.TIntersect(I)) + Assert.isTrue(TypeGuard.TLiteral(I.allOf[0])) + Assert.isTrue(TypeGuard.TLiteral(I.allOf[1])) + Assert.isTrue(TypeGuard.TLiteral(I.allOf[2])) + Assert.isTrue(TypeGuard.TLiteral(I.allOf[3])) + }) + it('Should Index 23', () => { + const A = Type.Object({ x: Type.Literal('A') }) + const B = Type.Object({ x: Type.Literal('B') }) + const C = Type.Object({ x: Type.Literal('C') }) + const D = Type.Object({ x: Type.Literal('D') }) + const T = Type.Union([A, B, C, D]) + const I = Type.Index(T, ['x']) + Assert.isTrue(TypeGuard.TUnion(I)) + Assert.isTrue(TypeGuard.TLiteral(I.anyOf[0])) + Assert.isTrue(TypeGuard.TLiteral(I.anyOf[1])) + Assert.isTrue(TypeGuard.TLiteral(I.anyOf[2])) + Assert.isTrue(TypeGuard.TLiteral(I.anyOf[3])) + }) + it('Should Index 24', () => { + const A = Type.Object({ x: Type.Literal('A'), y: Type.Number() }) + const B = Type.Object({ x: Type.Literal('B') }) + const C = Type.Object({ x: Type.Literal('C') }) + const D = Type.Object({ x: Type.Literal('D') }) + const T = Type.Intersect([A, B, C, D]) + const I = Type.Index(T, ['x', 'y']) + Assert.isTrue(TypeGuard.TUnion(I)) + Assert.isTrue(TypeGuard.TIntersect(I.anyOf[0])) + Assert.isTrue(TypeGuard.TNumber(I.anyOf[1])) + }) + it('Should Index 25', () => { + const A = Type.Object({ x: Type.Literal('A'), y: Type.Number() }) + const B = Type.Object({ x: Type.Literal('B'), y: Type.String() }) + const C = Type.Object({ x: Type.Literal('C') }) + const D = Type.Object({ x: Type.Literal('D') }) + const T = Type.Intersect([A, B, C, D]) + const I = Type.Index(T, ['x', 'y']) + Assert.isTrue(TypeGuard.TUnion(I)) + Assert.isTrue(TypeGuard.TIntersect(I.anyOf[0])) + Assert.isTrue(TypeGuard.TIntersect(I.anyOf[1])) + Assert.isTrue(TypeGuard.TNumber(I.anyOf[1].allOf[0])) + Assert.isTrue(TypeGuard.TString(I.anyOf[1].allOf[1])) + }) + it('Should Index 26', () => { + const T = Type.Recursive((This) => + Type.Object({ + x: Type.String(), + y: Type.Number(), + z: This, + }), + ) + const I = Type.Index(T, ['x', 'y', 'z']) + Assert.isTrue(TypeGuard.TUnion(I)) + Assert.isTrue(TypeGuard.TString(I.anyOf[0])) + Assert.isTrue(TypeGuard.TNumber(I.anyOf[1])) + Assert.isTrue(TypeGuard.TThis(I.anyOf[2])) + }) }) diff --git a/test/static/indexed.ts b/test/static/indexed.ts index 629582b..aa27690 100644 --- a/test/static/indexed.ts +++ b/test/static/indexed.ts @@ -62,3 +62,164 @@ import { Type, Static } from '@sinclair/typebox' Expect(R).ToInfer() } +// ------------------------------------------------------------------ +// Intersections +// ------------------------------------------------------------------ +{ + type A = { x: string; y: 1 } + type B = { x: string; y: number } + type C = A & B + type R = C['y'] + + const A = Type.Object({ x: Type.String(), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.String(), y: Type.Number() }) + const C = Type.Intersect([A, B]) + const R = Type.Index(C, ['y']) + Expect(R).ToBe<1>() +} +{ + type A = { x: string; y: 1 } + type B = { x: string; y: number } + type C = A & B + type R = C['x'] + + const A = Type.Object({ x: Type.String(), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.String(), y: Type.Number() }) + const C = Type.Intersect([A, B]) + const R = Type.Index(C, ['x']) + Expect(R).ToBe() +} +{ + type A = { x: string; y: 1 } + type B = { x: string; y: number } + type C = A & B + type R = C['x' | 'y'] + + const A = Type.Object({ x: Type.String(), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.String(), y: Type.Number() }) + const C = Type.Intersect([A, B]) + const R = Type.Index(C, ['x', 'y']) + Expect(R).ToBe() +} +{ + type A = { x: string; y: number } + type B = { x: number; y: number } + type C = A & B + type R = C['x'] + + const A = Type.Object({ x: Type.String(), y: Type.Number() }) + const B = Type.Object({ x: Type.Number(), y: Type.Number() }) + const C = Type.Intersect([A, B]) + const R = Type.Index(C, ['x']) + Expect(R).ToBe() +} +{ + type A = { x: string; y: number } + type B = { x: number; y: number } + type C = A & B + type R = C['y'] + + const A = Type.Object({ x: Type.String(), y: Type.Number() }) + const B = Type.Object({ x: Type.Number(), y: Type.Number() }) + const C = Type.Intersect([A, B]) + const R = Type.Index(C, ['y']) + Expect(R).ToBe() +} +{ + type A = { x: string; y: number } + type B = { x: number; y: number } + type C = A & B + type R = C['x' | 'y'] + + const A = Type.Object({ x: Type.String(), y: Type.Number() }) + const B = Type.Object({ x: Type.Number(), y: Type.Number() }) + const C = Type.Intersect([A, B]) + const R = Type.Index(C, ['x', 'y']) + Expect(R).ToBe() +} +{ + type A = { x: string; y: 1 } + type B = { x: string; y: number } + type C = { x: string; y: number } + type D = { x: string } + type I = (A & B) & (C & D) + type R = I['x' | 'y'] + + const A = Type.Object({ x: Type.String(), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.String(), y: Type.Number() }) + const C = Type.Object({ x: Type.String(), y: Type.Number() }) + const D = Type.Object({ x: Type.String() }) + const I = Type.Intersect([Type.Intersect([A, B]), Type.Intersect([C, D])]) + const R = Type.Index(I, ['x', 'y']) + type O = Static + Expect(R).ToBe() +} +{ + type A = { x: string; y: 1 } + type B = { x: number; y: number } + type C = { x: string; y: number } + type D = { x: string } + type I = (A & B) & (C & D) + type R = I['x' | 'y'] + + const A = Type.Object({ x: Type.String(), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.Number(), y: Type.Number() }) + const C = Type.Object({ x: Type.String(), y: Type.Number() }) + const D = Type.Object({ x: Type.String() }) + const I = Type.Intersect([Type.Intersect([A, B]), Type.Intersect([C, D])]) + const R = Type.Index(I, ['x', 'y']) + type O = Static + Expect(R).ToBe<1>() +} +{ + type A = { x: string; y: 1 } + type B = { x: number; y: number } + type C = { x: string; y: number } + type D = { x: string } + type I = (A | B) & (C & D) + type R = I['x' | 'y'] + + const A = Type.Object({ x: Type.String(), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.Number(), y: Type.Number() }) + const C = Type.Object({ x: Type.String(), y: Type.Number() }) + const D = Type.Object({ x: Type.String() }) + const I = Type.Intersect([Type.Union([A, B]), Type.Intersect([C, D])]) + const R = Type.Index(I, ['x', 'y']) + type O = Static + Expect(R).ToBe() +} +{ + type A = { x: 'A'; y: 1 } + type B = { x: 'B'; y: number } + type C = { x: 'C'; y: number } + type D = { x: 'D' } + type I = A | B | C | D + type R = I['x'] + + const A = Type.Object({ x: Type.Literal('A'), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.Literal('B'), y: Type.Number() }) + const C = Type.Object({ x: Type.Literal('C'), y: Type.Number() }) + const D = Type.Object({ x: Type.Literal('D') }) + const I = Type.Union([A, B, C, D]) + const R = Type.Index(I, Type.Union([Type.Literal('x')])) + type O = Static + Expect(R).ToBe<'A' | 'B' | 'C' | 'D'>() +} +{ + type I = { + x: string + y: number + z: I + } + type R = I['x' | 'y' | 'z'] + const I = Type.Recursive((This) => + Type.Object({ + x: Type.String(), + y: Type.Number(), + z: This, + }), + ) + const R = Type.Index(I, ['x', 'y', 'z']) // z unresolvable + type O = Static + Expect(R).ToBe() +}