Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to describe enums and still get typescript types? #216

Closed
hexpunk opened this issue Oct 16, 2018 · 13 comments · May be fixed by #366
Closed

How to describe enums and still get typescript types? #216

hexpunk opened this issue Oct 16, 2018 · 13 comments · May be fixed by #366

Comments

@hexpunk
Copy link

hexpunk commented Oct 16, 2018

I've studied the various approaches in #67 and I've done a bit of my own code to try to crack this nut, but I can't seem to figure out how to represent enums with io-ts and also get all the necessary typescript hints. The various approaches suggested in that ticket and they work at runtime, but I lose all type hints at compile time. The result from a t.TypeOf or a decode on my enum types only show up to typescript as any. Furthermore, the values that are accepted by a function, for example, at compile time that expects the type of the t.TypeOf my io-ts enum accept all numbers or all strings instead of only the valid values associated with the given enum.

Is this a lost cause or is there some way to accurately get the full benefits of io-ts with enums?

@jrunning
Copy link

jrunning commented Nov 5, 2018

Literally the first thing I tried to do after installing this module today was to try to define an enum. I'm flummoxed, and wondering if it's worth trying to move forward with io-ts at all.

@smpalileo
Copy link

They need to be literals of your valid values.

For example:
enum = ['a', 'b', 'c']

would have to be implemented like this:

a = t.literal('a')
b = t.literal('b')
c = t.literal('c')

enumVar = t.union([a,b,c])

@sgronblo
Copy link

I just stumbled upon this issue when looking for searching for enums. But isn't the example proposed by @smpalileo the one that is shown as a bad practice here: https://github.com/gcanti/io-ts#union-of-string-literals ?

@omerkhawaja
Copy link

omerkhawaja commented Feb 7, 2019

Firstly, io-ts library is ground-breaking.

It's my first week using TypeScript, but how about this implementation?

Using this, I'm able to get all benefits of type inference for autocomplete.

// Define a regular enum
enum TestEnum {
  Foo = 'Foo',
  Bar = 'Bar',
}

// Create an Enum-like validator
const TestEnumV = t.keyof({  // using "keyof" for better performance instead of "union"
  [TestEnum.Foo]: null,
  [TestEnum.Bar]: null,
});

// Create a Product type validator
const ProductV = t.type({
  id: t.number,
  name: t.string,
  quantity: t.number,
  type: TestEnumV,
});

type Product = t.TypeOf<typeof ProductV>;
const item: Product = {
  id: 3,
  name: 'yo',
  quantity: 123,
  type: TestEnum.Foo,  // Use TypeScript's "enum" here instead of "TestEnumV"
};

const result = ProductV.is(item);
console.log('result', result); // => true

@noe132
Copy link

noe132 commented Mar 11, 2019

I wrote this to check enum types, works for both string or number enums, since enums are just simple objects

update 2020-4-19: add enum reverse mapping check for number enums

/* EnumType.ts */
import * as t from 'io-ts'

// EnumType Class
export class EnumType<A> extends t.Type<A> {
  public readonly _tag: 'EnumType' = 'EnumType'
  public enumObject!: object
  public constructor(e: object, name?: string) {
    super(
      name || 'enum',
      (u): u is A => {
        if (!Object.values(this.enumObject).find((v) => v === u)) {
          return false
        }
        // enum reverse mapping check
        if (typeof (this.enumObject as any)[u as string] === 'number') {
          return false
        }

        return true
      },
      (u, c) => (this.is(u) ? t.success(u) : t.failure(u, c)),
      t.identity,
    )
    this.enumObject = e
  }
}

// simple helper function
export const createEnumType = <T>(e: object, name?: string) => new EnumType<T>(e, name)
/* fruit.ts */
// fruit enum
enum FRUIT {
  APPLE = 'APPLE',
  BANANA = 'BANANA',
}

// fruit codec
const FruitItem = t.type({
  fruit: createEnumType<FRUIT>(FRUIT, 'FRUIT'),
  quatity: t.number,
})

const result1 = FruitItem.is({
  fruit: 'BANANA',
  quatity: 10,
})
expect(result1).toBe(true)

const result2 = FruitItem.is({
  fruit: 'apple',
  quatity: 8,
})
expect(result2).toBe(false)


type FruitStaticType = t.TypeOf<typeof FruitItem>
// FruitStaticType is expected to be same as below
type FruitStaticType = {
  fruit: FRUIT
  quatity: number
}

alecgibson pushed a commit to alecgibson/io-ts that referenced this issue Sep 17, 2019
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]: gcanti#216 (comment)
alecgibson pushed a commit to alecgibson/io-ts that referenced this issue Sep 17, 2019
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]: gcanti#216 (comment)
alecgibson pushed a commit to alecgibson/io-ts that referenced this issue Sep 17, 2019
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]: gcanti#216 (comment)
@samhh
Copy link

samhh commented Dec 5, 2019

In the meantime, here's a relatively clean alternative using fromRefinement in io-ts-types:

enum Thing {
    A = 'a',
    B = 'b',
}

const isThing: Refinement<unknown, Thing> = (arg): arg is Thing =>
    Object.values<unknown>(Thing).includes(arg);

const thingCodec = fromRefinement<Thing>('thing', isThing);

@geekflyer
Copy link

geekflyer commented Mar 14, 2020

I took @samhh's idea, removed the dependency on io-ts-types and turned this into a fromEnum utility function. Works great so far.

// this utility function can be used to turn a TypeScript enum into a io-ts codec.
export function fromEnum<EnumType>(enumName: string, theEnum: Record<string, string | number>) {
  const isEnumValue = (input: unknown): input is EnumType => Object.values<unknown>(theEnum).includes(input);

  return new t.Type<EnumType>(
    enumName,
    isEnumValue,
    (input, context) => (isEnumValue(input) ? t.success(input) : t.failure(input, context)),
    t.identity
  );
}

here's a usage example:

import { PathReporter } from "io-ts/lib/PathReporter";

enum Thing {
  FOO = "foo",
  BAR = "bar"
}

const ThingCodec = fromEnum<Thing>("Thing", Thing);

console.log(PathReporter.report(ThingCodec.decode('invalidvalue')));
// prints [ 'Invalid value "invalidvalue" supplied to : Thing' ]
console.log(PathReporter.report(ThingCodec.decode('foo')));
// prints [ 'No errors!' ]

@haysmike
Copy link

If you're only using string enums, I found a very slight improvement that allows omission of the type argument without it creating any types:

export function fromEnum<EnumType extends string>(
  enumName: string,
  theEnum: Record<string, EnumType>,
): t.Type<EnumType, EnumType, unknown> {
  const isEnumValue = (input: unknown): input is EnumType =>
    Object.values<unknown>(theEnum).includes(input);

  return new t.Type<EnumType>(
    enumName,
    isEnumValue,
    (input, context) =>
      isEnumValue(input) ? t.success(input) : t.failure(input, context),
    t.identity,
  );
}

Usage:

enum Thing {
  FOO = 'foo',
  BAR = 'bar',
}

const ThingCodec = fromEnum('Thing', Thing); // Can omit the type argument
type ThingType = t.TypeOf<typeof ThingCodec>;

const thing1: ThingType = Thing.FOO;

const thing2: ThingType = 'foo';
// `tsc` error TS2322: Type '"foo"' is not assignable to type 'Thing'.

console.log(PathReporter.report(ThingCodec.decode('invalidvalue')));
// prints [ 'Invalid value "invalidvalue" supplied to : Thing' ]
console.log(PathReporter.report(ThingCodec.decode('foo')));
// prints [ 'No errors!' ]

Of course with numeric enums, the reverse mapping breaks this 🤷

@gcanti
Copy link
Owner

gcanti commented Apr 30, 2020

Of course with numeric enums, the reverse mapping breaks this

Numeric enums shouldn't be used anyway, they are unsafe

export enum E {
  a = 1
}
declare function f(e: E): void
f(666)

@haysmike
Copy link

Numeric enums shouldn't be used anyway, they are unsafe

Completely agree!

@webberwang
Copy link

webberwang commented Aug 1, 2020

I wrote this to check enum types, works for both string or number enums, since enums are just simple objects

update 2020-4-19: add enum reverse mapping check for number enums

should merge this into https://github.com/gcanti/io-ts-types

@seyfer seyfer mentioned this issue Sep 1, 2020
@mendrik
Copy link

mendrik commented Dec 2, 2020

I took @samhh's idea, removed the dependency on io-ts-types and turned this into a fromEnum utility function. Works great so far.

// this utility function can be used to turn a TypeScript enum into a io-ts codec.
export function fromEnum<EnumType>(enumName: string, theEnum: Record<string, string | number>) {
  const isEnumValue = (input: unknown): input is EnumType => Object.values<unknown>(theEnum).includes(input);

  return new t.Type<EnumType>(
    enumName,
    isEnumValue,
    (input, context) => (isEnumValue(input) ? t.success(input) : t.failure(input, context)),
    t.identity
  );
}

here's a usage example:

import { PathReporter } from "io-ts/lib/PathReporter";

enum Thing {
  FOO = "foo",
  BAR = "bar"
}

const ThingCodec = fromEnum<Thing>("Thing", Thing);

console.log(PathReporter.report(ThingCodec.decode('invalidvalue')));
// prints [ 'Invalid value "invalidvalue" supplied to : Thing' ]
console.log(PathReporter.report(ThingCodec.decode('foo')));
// prints [ 'No errors!' ]

a bit better version that actually returns typesafe enum values:

export const fromEnum = <T extends string, TEnumValue extends string | number>(
  enumName: string,
  theEnum: { [key in T]: TEnumValue }
): t.Type<TEnumValue> => {
  const isEnumValue = (input: unknown): input is TEnumValue =>
    Object.values(theEnum).includes(input)

  return new t.Type<TEnumValue>(
    enumName,
    isEnumValue,
    (input, context) => (isEnumValue(input) ? t.success(input) : t.failure(input, context)),
    t.identity
  )
}

alecgibson pushed a commit to alecgibson/io-ts that referenced this issue Jan 4, 2021
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]: gcanti#216 (comment)
alecgibson pushed a commit to alecgibson/io-ts that referenced this issue Jan 4, 2021
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]: gcanti#216 (comment)
alecgibson pushed a commit to alecgibson/io-ts that referenced this issue Jan 4, 2021
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]: gcanti#216 (comment)
alecgibson pushed a commit to alecgibson/io-ts that referenced this issue Jan 4, 2021
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]: gcanti#216 (comment)
@mscottx88
Copy link

For those that find themselves here, another iteration on the theme.

/**
 * Produce a Map populated with an enumeration's inverted key-value pair(s).
 */
export type ReverseMap<T> = Map<{ [K in keyof T]: T[K] }[keyof T], keyof T>;

/**
 * The Utils service class provides a collection of convenience methods.
 */
export class Utils {
  /**
   * Given an enumeration created in Typescript, produce an io-ts validator schema.
   *
   * @see https://github.com/gcanti/io-ts/issues/216
   *
   * @example
   * ```typescript
   * enum Foo {
   *   bar = 'baz',
   * }
   *
   * const TFizz = t.type({
   *   buzz: Utils.enum<Foo>(Foo),
   * });
   * ```
   *
   * @param {E} enumeration
   * The enumeration object.
   *
   * @param {string} name
   * An optional name of the enumeration type.
   *
   * @returns {t.Type<T>}
   * An io-ts validation schema for the enumeration.
   */
  public static enum<T, E extends object = object>(
    enumeration: E,
    name: string = 'enum',
  ): t.Type<T> {
    return new (class extends t.Type<T> {
      public readonly reverseMap: ReverseMap<E>;

      constructor() {
        super(
          name,
          (value: unknown): value is T => this.reverseMap.has(value as E[keyof E]),
          (value: unknown, context: t.Context): either.Either<t.Errors, T> =>
            this.is(value) ? t.success(value) : t.failure(value, context),
          t.identity,
        );

        this.reverseMap = Utils.enumReverseMap(enumeration);
      }
    })();
  }

  /**
   * Given an arbitrary enumeration, generate the reverse lookup map.
   *
   * @param {object} enumeration
   * The enumeration.
   *
   * @returns {Map}
   * A Mapping where the key is the value and the value is the key.
   */
  public static enumReverseMap<E extends object>(enumeration: E): ReverseMap<E> {
    return new Map(Object.entries(enumeration).map(([key, value]) => [value, key as keyof E]));
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.