-
Notifications
You must be signed in to change notification settings - Fork 331
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
Comments
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. |
They need to be literals of your valid values. For example: would have to be implemented like this: a = t.literal('a') enumVar = t.union([a,b,c]) |
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 ? |
Firstly, 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 |
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
}
|
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)
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)
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)
In the meantime, here's a relatively clean alternative using 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); |
I took @samhh's idea, removed the dependency on // 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!' ] |
If you're only using string enums, I found a very slight improvement that allows omission of the type argument without it creating 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 🤷 |
Numeric enums shouldn't be used anyway, they are unsafe export enum E {
a = 1
}
declare function f(e: E): void
f(666) |
Completely agree! |
should merge this into https://github.com/gcanti/io-ts-types |
a bit better version that actually returns typesafe enum values:
|
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)
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)
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)
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)
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]));
}
} |
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 adecode
on my enum types only show up to typescript asany
. Furthermore, the values that are accepted by a function, for example, at compile time that expects the type of thet.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?
The text was updated successfully, but these errors were encountered: