Skip to content

Commit

Permalink
Add enum support
Browse files Browse the repository at this point in the history
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]: #216 (comment)
  • Loading branch information
Alec Gibson committed Jan 4, 2021
1 parent f13a10a commit 9a34f36
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 18 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
71 changes: 53 additions & 18 deletions docs/modules/index.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -135,69 +137,70 @@ 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)
- [TypeOfPartialProps (type alias)](#typeofpartialprops-type-alias)
- [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)
Expand Down Expand Up @@ -1546,6 +1549,28 @@ readonly _tag: "DictionaryType"

Added in v1.0.0

## EnumType (class)

**Signature**

```ts
export declare class EnumType<E> {
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**
Expand Down Expand Up @@ -2460,6 +2485,16 @@ export declare const appendContext: (c: Context, key: string, decoder: Decoder<a

Added in v1.0.0

## enum

**Signature**

```ts
export declare const enum: <E extends typeof Enum>(e: E, name?: string) => EnumType<E>
```

Added in v2.1.0

## exact

Strips additional properties
Expand Down
40 changes: 40 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,39 @@ export const array = <C extends Mixed>(item: C, name: string = `Array<${item.nam
item
)

enum Enum {}
/**
* @since 2.3.0
*/
export class EnumType<E extends typeof Enum> extends Type<E[keyof E]> {
/**
* @since 2.3.0
*/
readonly _tag: 'EnumType' = 'EnumType'
private readonly _enum: E
private readonly _enumValues: Set<string | number>
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 extends typeof Enum>(e: E, name?: string) => new EnumType<E>(e, name)

/**
* @since 1.0.0
*/
Expand Down Expand Up @@ -1871,6 +1904,13 @@ export {
undefinedType as undefined
}

export {
/**
* @since 2.1.0
*/
enumType as enum
}

export {
/**
* Use `UnknownArray` instead
Expand Down
85 changes: 85 additions & 0 deletions test/enum.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})

0 comments on commit 9a34f36

Please sign in to comment.