diff --git a/example/trpc/readme.md b/example/trpc/readme.md index 5a2b361..adcb43a 100644 --- a/example/trpc/readme.md +++ b/example/trpc/readme.md @@ -44,21 +44,21 @@ export const appRouter = t.router({ ## RpcType -Unlike Zod, Yup, Superstruct, TypeBox types are pure JSON Schema and do not implement built a in `parse` or `validate` function. Because of this, you will need to wrap the Type in a TRPC compatible function which implements `parse` or `validate` on behalf of TRPC. As JSON Schema is a formal specification, there are a number of ways you can choose to implement this function, the following shows recommended implementations. +Unlike Zod, Yup, Superstruct, TypeBox types are pure JSON Schema and do not implement a built in `parse()` or `validate()` function. Because of this, you will need to wrap the Type in a TRPC compatible function which implements `parse()` on behalf of TRPC. As JSON Schema is a formal specification, there are a number of ways you may choose to implement this function. The following are some recommended implementations. -### With [TypeCompiler](https://www.typescriptlang.org/dev/bug-workbench/?target=99&lib=true&ts=4.7.4#code/PTAEAEDMEsBsFMB2BDAtvAXKATgBwMYAuAnrvAHSEDOAUNKrgPbaGgDeoAKgMr4AW8VMlABfUJGyNUoAOTgq0RPljJo2YCTIAjRgA8ZdBs1YdOpeAGEpuOPGyjxk6XIVKVajeZ27g+a7ewDeiYWdi4AJQAFCwBRbEl7MQkpWXBCPHxgKjsANzsDGnhdENZIAFclQmhGRFBwgjMyAB5OUCLCJAATKi5eASEAPgAKKn5BZCxOABoceEg7JHx4Kkm+8YBtAF1QAF5QLYBKdhpQUD9EKlYx-ABrXa5zKwYA8iebBBGxoRnsOYWlZYHE6zQhlbC1IY5ZCwMqYUAVG6IRgAd0QRx2A2Op1O0EgQ2uN1eAlukOhsIOR1+oPBoChMPgwNO50uYVwyEIfBm6CoVGQAHN4A49utyKKCeQ4gkqKT6QdNusAAybRmgDmSZGgRDwDWcKKxeLMIYcbm8gVYAAGABI2Cb+fAksxQNa2RyROaZn5OnCZAAhACCABEAPrhGIARQAqjFuJwZKIgacRDQkzQQBAYAgUOgsJIyh1sJRaMFjGFFNBCLrog5ks40hksrl8oYSmFGoKkk5Uq5lKp1Jp4N4gkZQhx6vg29XOzJyMAMv2CszWMhOp0AJKIXB5+5jttDNvkADyWgAVvAiEbgRMHmRyAA5MqoLR2IYHKbArSTcx3h9P7Av5MUjQNCLqAy6dAeeabqwew7uYe5fvej7PoBwE1Cy0GgGWFZ6uQ+C-Oy8D-oUxQliByC4Lg4SMHmdj3IQ5C5vmF6nGBWD0bgkhLJ0YIMtioDkIoUFDGB65QQm2LkNRhBCWBEHSXm4mnOQACOsLYMQQxGphG5biI6KYoJebkMIADU2lQeQWhHGm-YALS8vMAFAA) +### With [TypeCompiler](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgFQJ5gKYGELmAGwyjgF84AzKXOAcgAEBnYAOwGN8BDYKAehnQwAjCAA8erXGAJEaAWABQoSLEQoAyqwAWGEB1JwDhytXpM2nbnwHCRcxeGjwkyAEoAFLAFEoVYmSNUILR0MFBgrDwMRABuMgoKGCLK8OQArmwwwBDMcC7haJgAPMhwiTAYzAAmDOpaOhwAfAAUDHW6AFwoADRwUBjkRBWsGAydyBraugDaALpwALxwswCUiAoGEswM8HWsANYLKAI4eIRQAHQnUoQtbRw9fQN9bCPL670YMKlQOU3RHPhUhhOuk9swIAB3ZireYNNbyQxwYDkOBNXZ7S7afZ-AFA5arPpfH5wf6AjDvDbZbaqMAcGCaHogEYMDgAcww+kW6PO3l8DBxZOW5wAYtxtk1lgBCClwelUCFwZgYBWuDy86BNJBMhgs9mdAAGABIENrdRgyORoHBjbT6SR9T0JJVgbQAEIAQQAIgB9FyeACKAFVPGpkDRSG8EaQFCQgA) The following uses the TypeCompiler to perform JIT optimized type checking for TRPC. ```typescript -import { TSchema } from '@sinclair/typebox' import { TypeCompiler } from '@sinclair/typebox/compiler' +import { TSchema } from '@sinclair/typebox' import { TRPCError } from '@trpc/server' -export function RpcType(schema: T) { - const check = TypeCompiler.Compile(schema) +export function RpcType(schema: T, references: TSchema[] = []) { + const check = TypeCompiler.Compile(schema, references) return (value: unknown) => { if (check.Check(value)) return value const { path, message } = check.Errors(value).First()! @@ -69,18 +69,18 @@ export function RpcType(schema: T) { -### With [Value](https://www.typescriptlang.org/dev/bug-workbench/?target=99&lib=true&ts=4.7.4#code/PTAEAEDMEsBsFMB2BDAtvAXKATgBwMYAuAnrvAHSEDOAUNKrgPbaGgDeoAasrAK7ygAvqEjZGqUAHJwVaInyxk0bMBJkARowAewAG49+kug2asOAFQDK+ABbxUyISLETps+YuWrS8TVqP0TCzsoOYASgAKAMIAothi2E6i4lLghHj4wFTw2Lo5RjTwWkGskLzyhNCMiKBhBOY+ADzmoEWESAAmVKHWdg4AfAAUVLb2yFjmADQ48JA5SPjwVBO9YwDaALqgALygmwCU7DSgM4S82DWD+nyYoOUA1oiMAO6Ih9v9Rycn0JCD3DdyFE7Ph7sNRg5ptd+PtDth4GcLqBofBjt98NUqGZQLhkIQbNN0FQqMgAOYCYS7NbkGkA-jkOIJKjgvrIabwubw+RLKEGeD7DZrAAMGzRJ3xYmeoEQ8Cl4WijOYgw4RJJ5KwAAMACRsVVk+DCSDMUA63H4wQa6YYjq3SQAIQAggARAD6YRiAEUAKoxSzmSRCfZowQ0EM0EAQGAIFDoLBiXjtbCUWiBUwhOTQQjyqJJFypdIELI5PLYAImYIWHy5lJuOQKJQqNS+bRlkohOr4BpkauucjADJNgoYxBY0DIDodACSiFwCZ2tXqPkGXYoAHl1AAreBEZVo8ahHzkAByvFQ6hyg32kzR6gmh5PZ4vQcEsJoNGHo-HHVXCdnrF2HYrsu96nue2CXkG76Yqw-6gBmWaRFE5D4PCeLwJeb5FG2H6sMguC4GEjAJjk86EOQ8aJruJxflgZG4GIiwdOcqLfKA5ByH+gxftOf5Bqx5BEYQnFfj+QkJnx3zkAAjvw2DEIMypwTOc4vjsnwcQm5COAA1Epf7kOohwRk2AC0JJzKG+xAA) +### With [Value](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgNQIYBsCuBTOBfOQwgMyghDgHIABAZ2ADsBjdVYKAehgE8xsAjCAA8OANww5KAWABQoSLERwAKgGUmAC2whU+IqXJU6jFm048+godLnho8JMoBKABQDCAUShkoegxRoYKDAmDlpsKFEIm1lsIQV4YkxmGGAIBjgnEOVebAAeZTg4mGwGABNaFXUtHQA+AApaTW1UAC4VABo4KGxiCNKmbFp2tWadAG0AXTgAXjgpgEpEWUIemEwoDPrxLGx25IBrBggAdwYlmdrlmSI4YGI4erRdgDo3LSYDxrHULp6+nrMIZdHY4BZLNYbDKg7ArIhMdK0BxwMCoGAaLogIa0VAAc1wBDmzxwLy8Plo3xqvzgMIWLwAYuwkfUFgBCOGEdFkE5wBjYHnOdxk6D1JBY2g4-HtAAGABIEOLJdgCMRoHB5aj0XhpV0EWU9lQAEIAQQAIgB9JweACKAFUPKplJR8As4XhZHggA) -The following performs dynamic type checking without code evaluation. +The following uses dynamic type checking without code evaluation. This is slower to validate, but may be required in restricted code evaluation environments. ```typescript import { Value } from '@sinclair/typebox/value' import { TSchema } from '@sinclair/typebox' import { TRPCError } from '@trpc/server' -export function RpcType(schema: T) { +export function RpcType(schema: T, references: TSchema[] = []) { return (value: unknown) => { - if (Value.Check(schema, value)) return value + if (Value.Check(schema, references, value)) return value const { path, message } = Value.Errors(schema, value).First()! throw new TRPCError({ message: `${message} for ${path}`, code: 'BAD_REQUEST' }) } @@ -89,7 +89,7 @@ export function RpcType(schema: T) { -### With [Ajv](https://www.typescriptlang.org/dev/bug-workbench/?target=99&lib=true&ts=4.7.4#code/PTAEAEDMEsBsFMB2BDAtvAXKATgBwMYAuAnrvAHSEDOAUNKrgPbaGgDeoAKgMr4AW8VMgA0oboWSFo+UAF9QkbI1SgA5OCrRE+WMmjZgJMgCNGAD1V0GzVh04AlAAoBhAKLYl2OQqUr1hPHxgKnhsADdQy3omFlAAQQArMJ9lNWQk4AATaCpCYAAmAAYiyxp4MxjWSABXbSlGRFB7Ak5SeAAeTlBywiRMqi5eASEAPgAKKn5BZCxOUWx4SFCkfHgqWaHpgG0AXVAAXlBdgEp2GlBQfAbc0HTkw8R4AHd4pLHj85xF5e018khmK5kPwxmMFksFr9TvsRrckuRkJlMpshGDvpDVscPhcrogblN8ABrA5wsLkK4MODwCZTITYr6EarYRpjMLIWDVTCgWqExCMJ6IY5YcSSaSdWEws4XC7QSBjAmE1nszlYhlMxpsjnwT44662UDoKhUZAAc3goi0uWQv0ckj43kOCvIoU8VAAhFtCjsdaBCHwlC9Hi8HC53J4xhxDcazVgAAYAEjYUdN8HkAK8ictEhtdtksdEV0yXNUACE4gARAD69lcAEUAKqubicVRyemyGgdmggCAwBAodBYJTVXrYSi0aI2digLTQQgh5zeRSpfyBYKhCLYKLWWJ2NpL3xqDRaHR6AxGeCmCxWSrT5r4VpkA8r8jAQIX0q4m6IzIASUQuAjiS96PtSoHkAA8sYCTwEQEafDMXBtOQABy1SoMYoTvMInzGLMyFoRhWEfLIWI0DQX6sD+EEjoBrCHCBbRjOBhGYdg7wfBReq+iSs7zk4zjkgskjUpx5S3pRty4Lg9iMCOoQkoQ5DDqO8EXD+WBKbgSirJkTLatKoDkFodFjD+-50fSFzkHJhCmdRtEjlZRkAI6ctgxCghwJlAaRBywj5SnIKAADUM4ASO5DGKcPYXgAtMaSydscQA) +### With [Ajv](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgFQMoGMAWBTEBDAGjlRjxmHTgF84AzKCEOAcgAEBnYAO3QBs9gUAPQwAnmGwAjCAA9mAWABQoSLEQoASgAUAwgFEoDKNTimzdBkzYwoYdEPbYoANycLl4aPACCAK2cWjCx4-kIAJsDsMEIATAAM8e5K2DKq8LQArjzkEFxwGnbI4tgAPMhwKTDYXGHsKBg4+AB8ABTsWLh4AFwoRFDYtE7V6NjsPWgd+ADaALpwALxwswCUiEqm6LlRcCEBi1zYAO5wfs4ty+tw-YP9PKMAdLTQenhYLS3XQ3er8007-vc8GEwg1Oh8Bl8RssLooNlt4B10ABrBb-Zz3TbgYC8bBtSZ4GGmfowDJQPItZx4XgZbA9LJIrgQQ5cZY9EhkChlP6-NawszAWhwFqIpEUqk06FXbAkslwSnU7CXOFcbZIECjdh4ADm2CI3CieDuWjImBMixF9ycRnYAEIpnEZkq4DBMAxjgdjshtPpDNAWmqNdraXAAAYAEgQ6vYmp1NCexgj+tIRpNVBDRE2YWDzAAQt4ACIAfQ0egAigBVPSoZDMaiE6hKKhAA) The following uses Ajv to perform more generalized JSON Schema checks across the complete JSON Schema specification. diff --git a/package-lock.json b/package-lock.json index 43f8a0a..e330584 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.28.6", + "version": "0.28.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.28.6", + "version": "0.28.7", "license": "MIT", "devDependencies": { "@sinclair/hammer": "^0.17.1", diff --git a/package.json b/package.json index 4b01987..8883728 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.28.6", + "version": "0.28.7", "description": "JSONSchema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", diff --git a/src/typebox.ts b/src/typebox.ts index 65feb59..3a4a7bd 100644 --- a/src/typebox.ts +++ b/src/typebox.ts @@ -369,7 +369,7 @@ export interface TIntersect extends TSchema, In // -------------------------------------------------------------------------- // prettier-ignore export type TKeyOfProperties = Static extends infer S - ? UnionToTuple<{[K in keyof S]: TLiteral<`${Assert}`>}[keyof S]> + ? UnionToTuple<{[K in keyof S]: TLiteral>}[keyof S]> : [] // prettier-ignore export type TKeyOfIndicesArray = UnionToTuple @@ -1943,7 +1943,8 @@ export namespace IndexedAccessor { return Type.Never() } export function Resolve(schema: TSchema, keys: Key[], options: SchemaOptions = {}): TSchema { - return Type.Union(keys.map((key) => Visit(schema, key.toString()))) + // prettier-ignore + return Type.Union(keys.map((key) => Visit(schema, key.toString())), options) } } // -------------------------------------------------------------------------- diff --git a/test/runtime/type/guard/indexed.ts b/test/runtime/type/guard/indexed.ts index bc47ba6..8222672 100644 --- a/test/runtime/type/guard/indexed.ts +++ b/test/runtime/type/guard/indexed.ts @@ -233,4 +233,48 @@ describe('type/guard/TIndex', () => { Assert.isTrue(TypeGuard.TNumber(I.anyOf[1])) Assert.isTrue(TypeGuard.TThis(I.anyOf[2])) }) + it('Should Index 27', () => { + const T = Type.Object({ + 0: Type.String(), + 1: Type.Number(), + }) + const I = Type.Index(T, [0, 1]) + Assert.isTrue(TypeGuard.TUnion(I)) + Assert.isTrue(TypeGuard.TString(I.anyOf[0])) + Assert.isTrue(TypeGuard.TNumber(I.anyOf[1])) + }) + it('Should Index 28', () => { + const T = Type.Object({ + 0: Type.String(), + '1': Type.Number(), + }) + const I = Type.Index(T, [0, '1']) + Assert.isTrue(TypeGuard.TUnion(I)) + Assert.isTrue(TypeGuard.TString(I.anyOf[0])) + Assert.isTrue(TypeGuard.TNumber(I.anyOf[1])) + }) + it('Should Index 29', () => { + const T = Type.Object({ + 0: Type.String(), + '1': Type.Number(), + }) + const I = Type.Index(T, Type.Union([Type.Literal(0), Type.Literal('1')])) + Assert.isTrue(TypeGuard.TUnion(I)) + Assert.isTrue(TypeGuard.TString(I.anyOf[0])) + Assert.isTrue(TypeGuard.TNumber(I.anyOf[1])) + }) + it('Should Index 30', () => { + const T = Type.Object({ + 0: Type.String(), + '1': Type.Number(), + }) + const I = Type.Index(T, Type.Union([Type.Literal(0), Type.Literal(1)])) + Assert.isTrue(TypeGuard.TUnion(I)) + Assert.isTrue(TypeGuard.TString(I.anyOf[0])) + Assert.isTrue(TypeGuard.TNumber(I.anyOf[1])) + // Note: Expect TNever for anyOf[1] but permit for TNumber due to IndexedAccess + // Resolve() which currently cannot differentiate between string and numeric keys + // on the object. This may be resolvable in later revisions, but test for this + // fall-through to ensure case is documented. For review. + }) }) diff --git a/test/static/indexed.ts b/test/static/indexed.ts index aa27690..c2cf5f9 100644 --- a/test/static/indexed.ts +++ b/test/static/indexed.ts @@ -6,60 +6,60 @@ import { Type, Static } from '@sinclair/typebox' x: Type.Number(), y: Type.String(), }) - const I = Type.Index(T, ['x', 'y']) - Expect(I).ToInfer() + const R = Type.Index(T, ['x', 'y']) + type O = Static + Expect(R).ToInfer() } { const T = Type.Tuple([Type.Number(), Type.String(), Type.Boolean()]) - const I = Type.Index(T, Type.Union([Type.Literal('0'), Type.Literal('1')])) - Expect(I).ToInfer() + const R = Type.Index(T, Type.Union([Type.Literal('0'), Type.Literal('1')])) + type O = Static + Expect(R).ToInfer() } { const T = Type.Tuple([Type.Number(), Type.String(), Type.Boolean()]) - const I = Type.Index(T, Type.Union([Type.Literal(0), Type.Literal(1)])) - Expect(I).ToInfer() + const R = Type.Index(T, Type.Union([Type.Literal(0), Type.Literal(1)])) + type O = Static + Expect(R).ToInfer() } { const T = Type.Object({ ab: Type.Number(), ac: Type.String(), }) - const I = Type.Index(T, Type.TemplateLiteral([Type.Literal('a'), Type.Union([Type.Literal('b'), Type.Literal('c')])])) - Expect(I).ToInfer() + + const R = Type.Index(T, Type.TemplateLiteral([Type.Literal('a'), Type.Union([Type.Literal('b'), Type.Literal('c')])])) + type O = Static + Expect(R).ToInfer() } { const A = Type.Tuple([Type.String(), Type.Boolean()]) - const R = Type.Index(A, Type.Number()) - + type O = Static Expect(R).ToInfer() } { const A = Type.Tuple([Type.String()]) - const R = Type.Index(A, Type.Number()) - + type O = Static Expect(R).ToInfer() } { const A = Type.Tuple([]) - const R = Type.Index(A, Type.Number()) - + type O = Static Expect(R).ToInfer() } { const A = Type.Object({}) - const R = Type.Index(A, Type.BigInt()) // Support Overload - + type O = Static Expect(R).ToInfer() } { const A = Type.Array(Type.Number()) - const R = Type.Index(A, Type.BigInt()) // Support Overload - + type O = Static Expect(R).ToInfer() } // ------------------------------------------------------------------ @@ -75,6 +75,7 @@ import { Type, Static } from '@sinclair/typebox' const B = Type.Object({ x: Type.String(), y: Type.Number() }) const C = Type.Intersect([A, B]) const R = Type.Index(C, ['y']) + type O = Static Expect(R).ToBe<1>() } { @@ -87,6 +88,7 @@ import { Type, Static } from '@sinclair/typebox' const B = Type.Object({ x: Type.String(), y: Type.Number() }) const C = Type.Intersect([A, B]) const R = Type.Index(C, ['x']) + type O = Static Expect(R).ToBe() } { @@ -99,6 +101,7 @@ import { Type, Static } from '@sinclair/typebox' const B = Type.Object({ x: Type.String(), y: Type.Number() }) const C = Type.Intersect([A, B]) const R = Type.Index(C, ['x', 'y']) + type O = Static Expect(R).ToBe() } { @@ -111,6 +114,7 @@ import { Type, Static } from '@sinclair/typebox' const B = Type.Object({ x: Type.Number(), y: Type.Number() }) const C = Type.Intersect([A, B]) const R = Type.Index(C, ['x']) + type O = Static Expect(R).ToBe() } { @@ -123,6 +127,7 @@ import { Type, Static } from '@sinclair/typebox' const B = Type.Object({ x: Type.Number(), y: Type.Number() }) const C = Type.Intersect([A, B]) const R = Type.Index(C, ['y']) + type O = Static Expect(R).ToBe() } { @@ -135,6 +140,7 @@ import { Type, Static } from '@sinclair/typebox' const B = Type.Object({ x: Type.Number(), y: Type.Number() }) const C = Type.Intersect([A, B]) const R = Type.Index(C, ['x', 'y']) + type O = Static Expect(R).ToBe() } { @@ -223,3 +229,24 @@ import { Type, Static } from '@sinclair/typebox' type O = Static Expect(R).ToBe() } +// ------------------------------------------------ +// Numeric | String Variants +// ------------------------------------------------ +{ + const T = Type.Object({ + 0: Type.Number(), + '1': Type.String(), + }) + const R = Type.Index(T, [0, '1']) + type O = Static + Expect(R).ToBe() +} +{ + const T = Type.Object({ + 0: Type.Number(), + '1': Type.String(), + }) + const R = Type.Index(T, Type.KeyOf(T)) + type O = Static + Expect(R).ToBe() +}