diff --git a/package-lock.json b/package-lock.json index 2a2ee6347..51286a331 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.31.13", + "version": "0.31.14", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.31.13", + "version": "0.31.14", "license": "MIT", "devDependencies": { "@sinclair/hammer": "^0.17.1", diff --git a/package.json b/package.json index fbb0e3392..13a376c7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.31.13", + "version": "0.31.14", "description": "JSONSchema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", diff --git a/src/typebox.ts b/src/typebox.ts index c4059ac31..3e4f0c420 100644 --- a/src/typebox.ts +++ b/src/typebox.ts @@ -176,6 +176,7 @@ export type TAnySchema = | TBoolean | TConstructor | TDate + | TEnum | TFunction | TInteger | TIntersect @@ -340,11 +341,12 @@ export interface TDate extends TSchema, DateOptions { export type TEnumRecord = Record export type TEnumValue = string | number export type TEnumKey = string -export type TEnumToLiteralUnion = T extends TEnumValue ? (string extends T ? TNever : TLiteral) : never -// ^ empty enums evaluate as string -export type TEnumToLiteralTuple = UnionToTuple> -export type TEnumToUnion = UnionType>> -export type TEnum = TEnumToUnion +export interface TEnum = Record> extends TSchema { + [Kind]: 'Union' + [Hint]: 'Enum' + static: T[keyof T] + anyOf: TLiteral[] +} // -------------------------------------------------------------------------- // TExtends // -------------------------------------------------------------------------- @@ -868,6 +870,7 @@ export type DecodeType = ( T extends TArray ? TArray> : T extends TAsyncIterator ? TAsyncIterator> : T extends TConstructor ? TConstructor> : + T extends TEnum ? TEnum : // intercept for union. interior non decodable T extends TFunction ? TFunction> : T extends TIntersect ? TIntersect> : T extends TIterator ? TIterator> : @@ -2899,12 +2902,11 @@ export class JsonTypeBuilder extends TypeBuilder { } /** `[Json]` Creates a Enum type */ public Enum>(item: T, options: SchemaOptions = {}): TEnum { - if (ValueGuard.IsUndefined(item)) return this.Never(options) as TEnum // prettier-ignore const values1 = Object.getOwnPropertyNames(item).filter((key) => isNaN(key as any)).map((key) => item[key]) as T[keyof T][] const values2 = [...new Set(values1)] const anyOf = values2.map((value) => Type.Literal(value)) - return this.Union(anyOf, options) as TEnum + return this.Union(anyOf, { ...options, [Hint]: 'Enum' }) as TEnum } /** `[Json]` Creates a Conditional type */ public Extends(left: L, right: R, trueType: T, falseType: U, options: SchemaOptions = {}): TExtends { diff --git a/test/runtime/value/transform/enum.ts b/test/runtime/value/transform/enum.ts new file mode 100644 index 000000000..1df8f7793 --- /dev/null +++ b/test/runtime/value/transform/enum.ts @@ -0,0 +1,45 @@ +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Enum', () => { + enum E { + A, + B, + C, + } + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Enum(E)) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Value.Decode(T0, E.A) + Assert.IsEqual(R, E.A) + }) + it('Should encode identity', () => { + const R = Value.Encode(T0, E.A) + Assert.IsEqual(R, E.A) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Value.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Enum(E)) + .Decode((value) => 1) + .Encode((value) => E.A) + it('Should decode mapped', () => { + const R = Value.Decode(T1, E.A) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Value.Encode(T1, null) + Assert.IsEqual(R, E.A) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Value.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/index.ts b/test/runtime/value/transform/index.ts index 2dc90ff81..bc4fb931a 100644 --- a/test/runtime/value/transform/index.ts +++ b/test/runtime/value/transform/index.ts @@ -6,6 +6,7 @@ import './bigint' import './boolean' import './constructor' import './date' +import './enum' import './function' import './integer' import './intersect' diff --git a/test/static/enum.ts b/test/static/enum.ts index b49472864..9818acfe1 100644 --- a/test/static/enum.ts +++ b/test/static/enum.ts @@ -33,7 +33,7 @@ import { Type } from '@sinclair/typebox' // expect empty enum to be string (as empty enums T[keyof T] evaluates as string) enum E {} const T = Type.Enum(E) - Expect(T).ToStaticNever() + Expect(T).ToStatic() } { // expect empty enum to be never diff --git a/test/static/record.ts b/test/static/record.ts index 35aa0bb13..9a2ccb639 100644 --- a/test/static/record.ts +++ b/test/static/record.ts @@ -78,9 +78,5 @@ import { Type, Static } from '@sinclair/typebox' C = 'Z', } const T = Type.Record(Type.Enum(E), Type.Number()) - Expect(T).ToStatic<{ - X: number - Y: number - Z: number - }>() + Expect(T).ToStatic<{}>() } diff --git a/test/static/transform.ts b/test/static/transform.ts index 6395e19ff..468787ef2 100644 --- a/test/static/transform.ts +++ b/test/static/transform.ts @@ -278,3 +278,16 @@ import { Expect } from './assert' Expect(T).ToStaticDecode() Expect(GenericIntersect(T)).ToStaticDecode<1>() } +{ + // should decode enum + enum E { + A, + B, + C, + } + const T = Type.Transform(Type.Enum(E)) + .Decode((value) => 1 as const) + .Encode((value) => E.A) + Expect(T).ToStaticDecode<1>() + Expect(T).ToStaticEncode() +}