From c676682daab857a31c275092509d24a6a53b036c 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 ++++ src/index.ts | 37 +++++++++++++++++++++++ test/enum.ts | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+) 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/src/index.ts b/src/index.ts index c2204bfbd..e2adcdbfd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -827,6 +827,36 @@ export const array = (item: C, name: string = `Array<${item.nam item ) +enum Enum {} +/** + * @since 2.3.0 + */ +export class EnumType extends Type { + public readonly _tag: 'EnumType' = 'EnumType' + private _enum: E + private _enumValues: Set + public 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 +1901,13 @@ export { undefinedType as undefined } +export { + /** + * @since 2.1.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) + }) + }) +})