From a048fbd436f377c412e2e07577b699a78465d702 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Mon, 4 Jan 2021 16:36:52 +0000 Subject: [PATCH] Add `enum` support The `enum` is a popular aspect of TypeScript, and can be embedded in interfaces as a concise, but descriptive, shorthand for literal string unions: ```typescript enum Colour { White = '000000', Black = 'ffffff' } ``` This change adds an exported member `enum` to `io-ts`, based on [this suggestion][1] by @noe132 It means that `enum`s can be reused directly in `io-ts`: ```typescript const T = t.enum(Colour) ``` [1]: https://github.com/gcanti/io-ts/issues/216#issuecomment-471497998 --- CHANGELOG.md | 5 +++ docs/modules/index.ts.md | 71 ++++++++++++++++++++++++--------- src/index.ts | 40 +++++++++++++++++++ test/enum.ts | 85 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 18 deletions(-) create mode 100644 test/enum.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c04639e9..893ced7a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ **Note**: Gaps between patch versions are faulty/broken releases. **Note**: A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice. +# 2.3.0 + +- **New Feature** + - Add support for `enum` + # 2.2.13 - **Bug Fix** diff --git a/docs/modules/index.ts.md b/docs/modules/index.ts.md index 0ff850a35..4c2533be3 100644 --- a/docs/modules/index.ts.md +++ b/docs/modules/index.ts.md @@ -124,9 +124,11 @@ Added in v1.0.0 - [Branded (type alias)](#branded-type-alias) - [DictionaryType (class)](#dictionarytype-class) - [\_tag (property)](#_tag-property-10) + - [EnumType (class)](#enumtype-class) + - [\_tag (property)](#_tag-property-11) - [ExactC (interface)](#exactc-interface) - [ExactType (class)](#exacttype-class) - - [\_tag (property)](#_tag-property-11) + - [\_tag (property)](#_tag-property-12) - [HasProps (type alias)](#hasprops-type-alias) - [HasPropsIntersection (interface)](#haspropsintersection-interface) - [HasPropsReadonly (interface)](#haspropsreadonly-interface) @@ -135,49 +137,49 @@ Added in v1.0.0 - [Int (type alias)](#int-type-alias) - [IntBrand (interface)](#intbrand-interface) - [InterfaceType (class)](#interfacetype-class) - - [\_tag (property)](#_tag-property-12) + - [\_tag (property)](#_tag-property-13) - [IntersectionC (interface)](#intersectionc-interface) - [IntersectionType (class)](#intersectiontype-class) - - [\_tag (property)](#_tag-property-13) + - [\_tag (property)](#_tag-property-14) - [KeyofC (interface)](#keyofc-interface) - [KeyofType (class)](#keyoftype-class) - - [\_tag (property)](#_tag-property-14) + - [\_tag (property)](#_tag-property-15) - [LiteralC (interface)](#literalc-interface) - [LiteralType (class)](#literaltype-class) - - [\_tag (property)](#_tag-property-15) + - [\_tag (property)](#_tag-property-16) - [Mixed (interface)](#mixed-interface) - [NullC (interface)](#nullc-interface) - [NullType (class)](#nulltype-class) - - [\_tag (property)](#_tag-property-16) + - [\_tag (property)](#_tag-property-17) - [NumberC (interface)](#numberc-interface) - [NumberType (class)](#numbertype-class) - - [\_tag (property)](#_tag-property-17) + - [\_tag (property)](#_tag-property-18) - [OutputOf (type alias)](#outputof-type-alias) - [OutputOfDictionary (type alias)](#outputofdictionary-type-alias) - [OutputOfPartialProps (type alias)](#outputofpartialprops-type-alias) - [OutputOfProps (type alias)](#outputofprops-type-alias) - [PartialC (interface)](#partialc-interface) - [PartialType (class)](#partialtype-class) - - [\_tag (property)](#_tag-property-18) + - [\_tag (property)](#_tag-property-19) - [Props (interface)](#props-interface) - [ReadonlyArrayC (interface)](#readonlyarrayc-interface) - [ReadonlyArrayType (class)](#readonlyarraytype-class) - - [\_tag (property)](#_tag-property-19) + - [\_tag (property)](#_tag-property-20) - [ReadonlyC (interface)](#readonlyc-interface) - [ReadonlyType (class)](#readonlytype-class) - - [\_tag (property)](#_tag-property-20) + - [\_tag (property)](#_tag-property-21) - [RecordC (interface)](#recordc-interface) - [RecursiveType (class)](#recursivetype-class) - - [\_tag (property)](#_tag-property-21) + - [\_tag (property)](#_tag-property-22) - [type (property)](#type-property) - [RefinementType (class)](#refinementtype-class) - - [\_tag (property)](#_tag-property-22) + - [\_tag (property)](#_tag-property-23) - [StringC (interface)](#stringc-interface) - [StringType (class)](#stringtype-class) - - [\_tag (property)](#_tag-property-23) + - [\_tag (property)](#_tag-property-24) - [TupleC (interface)](#tuplec-interface) - [TupleType (class)](#tupletype-class) - - [\_tag (property)](#_tag-property-24) + - [\_tag (property)](#_tag-property-25) - [TypeC (interface)](#typec-interface) - [TypeOf (type alias)](#typeof-type-alias) - [TypeOfDictionary (type alias)](#typeofdictionary-type-alias) @@ -185,19 +187,20 @@ Added in v1.0.0 - [TypeOfProps (type alias)](#typeofprops-type-alias) - [UndefinedC (interface)](#undefinedc-interface) - [UndefinedType (class)](#undefinedtype-class) - - [\_tag (property)](#_tag-property-25) + - [\_tag (property)](#_tag-property-26) - [UnionC (interface)](#unionc-interface) - [UnionType (class)](#uniontype-class) - - [\_tag (property)](#_tag-property-26) + - [\_tag (property)](#_tag-property-27) - [UnknownArrayC (interface)](#unknownarrayc-interface) - [UnknownC (interface)](#unknownc-interface) - [UnknownRecordC (interface)](#unknownrecordc-interface) - [UnknownType (class)](#unknowntype-class) - - [\_tag (property)](#_tag-property-27) + - [\_tag (property)](#_tag-property-28) - [VoidC (interface)](#voidc-interface) - [VoidType (class)](#voidtype-class) - - [\_tag (property)](#_tag-property-28) + - [\_tag (property)](#_tag-property-29) - [appendContext](#appendcontext) + - [enum](#enum) - [exact](#exact) - [failure](#failure) - [failures](#failures) @@ -1546,6 +1549,28 @@ readonly _tag: "DictionaryType" Added in v1.0.0 +## EnumType (class) + +**Signature** + +```ts +export declare class EnumType { + constructor(e: E, name?: string) +} +``` + +Added in v2.3.0 + +### \_tag (property) + +**Signature** + +```ts +readonly _tag: "EnumType" +``` + +Added in v2.3.0 + ## ExactC (interface) **Signature** @@ -2460,6 +2485,16 @@ export declare const appendContext: (c: Context, key: string, decoder: Decoder(e: E, name?: string) => EnumType +``` + +Added in v2.3.0 + ## exact Strips additional properties diff --git a/src/index.ts b/src/index.ts index c2204bfbd..45b8d8c0c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -827,6 +827,39 @@ export const array = (item: C, name: string = `Array<${item.nam item ) +enum Enum {} +/** + * @since 2.3.0 + */ +export class EnumType extends Type { + /** + * @since 2.3.0 + */ + readonly _tag: 'EnumType' = 'EnumType' + private readonly _enum: E + private readonly _enumValues: Set + constructor(e: E, name?: string) { + super( + name || 'enum', + (u): u is E[keyof E] => { + if (!this._enumValues.has(u as any)) return false + // Don't allow key names from number enum reverse mapping + if (typeof (this._enum as any)[u as string] === 'number') return false + return true + }, + (u, c) => (this.is(u) ? success(u) : failure(u, c)), + identity + ) + this._enum = e + this._enumValues = new Set(Object.values(e)) + } +} + +/** + * @since 2.3.0 + */ +const enumType = (e: E, name?: string) => new EnumType(e, name) + /** * @since 1.0.0 */ @@ -1871,6 +1904,13 @@ export { undefinedType as undefined } +export { + /** + * @since 2.3.0 + */ + enumType as enum +} + export { /** * Use `UnknownArray` instead diff --git a/test/enum.ts b/test/enum.ts new file mode 100644 index 000000000..4e79f4902 --- /dev/null +++ b/test/enum.ts @@ -0,0 +1,85 @@ +import * as assert from 'assert' +import * as t from '../src/index' +import * as _ from '../src/Decoder' +import { isLeft } from 'fp-ts/lib/Either' + +describe('enum', () => { + enum A { + Foo = 'foo', + Bar = 'bar' + } + + enum B { + Foo, + Bar + } + + describe('name', () => { + it('should assign a default name', () => { + const T = t.enum(A) + assert.strictEqual(T.name, 'enum') + }) + + it('should accept a name', () => { + const T = t.enum(A, 'T') + assert.strictEqual(T.name, 'T') + }) + }) + + describe('is', () => { + it('should check an enum string value', () => { + const T = t.enum(A) + assert.strictEqual(T.is(A.Foo), true) + assert.strictEqual(T.is('bar'), true) + assert.strictEqual(T.is('invalid'), false) + assert.strictEqual(T.is(null), false) + assert.strictEqual(T.is(A), false) + }) + + it('should check an enum integer value', () => { + const T = t.enum(B) + assert.strictEqual(T.is(B.Foo), true) + assert.strictEqual(T.is(1), true) + assert.strictEqual(T.is('Foo'), false) + assert.strictEqual(T.is('invalid'), false) + assert.strictEqual(T.is(null), false) + assert.strictEqual(T.is(B), false) + }) + }) + + describe('decode', () => { + it('should decode an enum string value', () => { + const T = t.enum(A) + assert.deepStrictEqual(T.decode(A.Foo), _.success(A.Foo)) + assert.deepStrictEqual(T.decode('bar'), _.success('bar')) + }) + + it('should decode an enum integer value', () => { + const T = t.enum(B) + assert.deepStrictEqual(T.decode(B.Foo), _.success(B.Foo)) + assert.deepStrictEqual(T.decode(1), _.success(1)) + }) + + it('should fail decoding an invalid string value', () => { + const T = t.enum(A) + assert.deepStrictEqual(isLeft(T.decode('invalid')), true) + }) + + it('should fail decoding an invalid integer value', () => { + const T = t.enum(B) + assert.deepStrictEqual(isLeft(T.decode(2)), true) + }) + }) + + describe('encode', () => { + it('should encode an enum string value', () => { + const T = t.enum(A) + assert.deepStrictEqual(T.encode(A.Foo), A.Foo) + }) + + it('should encode an enum integer value', () => { + const T = t.enum(B) + assert.deepStrictEqual(T.encode(B.Foo), B.Foo) + }) + }) +})