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 Sep 17, 2019
1 parent 647e530 commit 0fc277e
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 13 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.1.0

- **New Feature**
- Add support for `enum`

# 2.0.1

- **Bug Fix**
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ You can also use the [`withMessage`](https://gcanti.github.io/io-ts-types/module
| union | `A \| B` | `t.union([ A, B ])` |
| intersection | `A & B` | `t.intersection([ A, B ])` |
| keyof | `keyof M` | `t.keyof(M)` (**only supports string keys**) |
| enum | `enum A {}` | `t.enum(A)` |
| recursive types | | `t.recursion(name, definition)` |
| branded types / refinements | ✘ | `t.brand(A, predicate, brand)` |
| integer | ✘ | `t.Int` (built-in branded codec) |
Expand Down
43 changes: 31 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "io-ts",
"version": "2.0.1",
"version": "2.1.0",
"description": "TypeScript compatible runtime type system for IO validation",
"files": [
"lib",
Expand Down
25 changes: 25 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,24 @@ export const array = <C extends Mixed>(codec: C, name: string = `Array<${codec.n
codec
)

/**
* @since 2.1.0
*/
export class EnumType<A> extends Type<A> {
public readonly _tag: 'EnumType' = 'EnumType'
constructor(name: string, is: EnumType<A>['is'], validate: EnumType<A>['validate'], encode: EnumType<A>['encode']) {
super(name, is, validate, encode)
}
}

/**
* @since 2.1.0
*/
const enumType = <A>(e: any, name: string = 'Enum'): EnumType<A> => {
const is = (u: unknown): u is A => Object.keys(e).some(k => e[k] === u)
return new EnumType<A>(name, is, (u, c) => (is(u) ? success(u) : failure(u, c)), identity)
}

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

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

export {
/**
* Use `UnknownArray` instead
Expand Down
83 changes: 83 additions & 0 deletions test/enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as assert from 'assert'
import * as t from '../src/index'
import { assertSuccess, assertFailure } from './helpers'

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('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)
assertSuccess(T.decode(A.Foo), A.Foo)
assertSuccess(T.decode('bar'), 'bar')
})

it('should decode an enum integer value', () => {
const T = t.enum(B)
assertSuccess(T.decode(B.Foo), B.Foo)
assertSuccess(T.decode(1), 1)
})

it('should fail decoding an invalid string value', () => {
const T = t.enum(A)
assertFailure(T, 'invalid', ['Invalid value "invalid" supplied to : Enum'])
})

it('should fail decoding an invalid integer value', () => {
const T = t.enum(B)
assertFailure(T, 2, ['Invalid value 2 supplied to : Enum'])
})
})

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 0fc277e

Please sign in to comment.