Is there a way to cleanly express the equivalent of:
export enum Options {
optionA = 'optionA',
optionB = 'optionB',
optionC = 'optionC'
}
as runtime type and still get code completion?
(e.g. in an if clause if (val === Options.optionA))
I use something like that, but it's only for number enums, but you can modify it to work with strings
function getEnumValues(o: Object): number[] {
const re = /^-?[0-9]+$/
return Object.keys(o).filter(v => re.test(v)).map(v => parseInt(v))
}
export function mkEnum<E>(e: Object, name?: string) {
const values = getEnumValues(e)
const newType: t.Type<E> = {
_A: t._A,
name: name || "Enum",
validate: (v, c) => values.indexOf(v) >= 0 ? t.success<E>(v) : t.failure<E>(v, c)
}
return newType
}
// Example
enum Test { A, B, C }
const TestEnumType = T.mkEnum<Test>(Test, "TestEnumType")
type TestEnum = t.TypeOf<typeof TestEnumType>
const e1: TestEnum = Test.A
expect(t.is(e1, TestEnumType)).to.be.true
expect(t.is(100, TestEnumType)).to.be.false
@vegansk I think I found a slightly simpler way that works for me, but in general it's similar to yours:
enum Options {
optionA = 'optionA',
optionB = 'optionB'
}
const OptionsRTT: t.Type<Options> = {
_A: t._A,
name: 'Options',
validate: (value, context) => (value in Options) ? t.success(value) : t.failure<Options>(value, context)
}
const exampleVal: Options = Options.optionA;
console.dir(t.is(Options.optionA, OptionsRTT)); // prints`true`
could probably extract it into a helper but for now I just need it for one particular enum, so i'll leave it.
@geekflyer Your example checks equality against the enum key name, but the enum value could differ.
@vegansk How come your reg exp allows numeric enum values to start with a -? (/^-?[0-9]+$/)
@OliverJAsh:
> enum B { a = -1, b, c }
undefined
> console.log(B.a)
-1
undefined
> console.log(B)
{ '0': 'b', '1': 'c', a: -1, '-1': 'a', b: 0, c: 1 }
Here is an example that works with string enums (only), using the enum string value, not the key:
import * as t from 'io-ts';
import { PathReporter } from 'io-ts/lib/PathReporter'
// https://github.com/gcanti/io-ts/issues/67
const getObjectValues = (obj: {}): string[] =>
Object.keys(obj).reduce<string[]>((acc, key) => [ ...acc, (obj as any)[key] ], [])
export const createEnum = <E>(e: {}, name: string): t.Type<E> => {
const values = getObjectValues(e);
const newType: t.Type<E> = {
_A: t._A,
name,
validate: (v, c) =>
values.includes(v) ? t.success<E>(v) : t.failure<E>(v, c),
};
return newType;
};
enum KeysT {
foo = 'fooValue',
bar = 'barValue',
}
const Keys = createEnum<KeysT>(
KeysT,
'Keys',
);
const goodVal = {
fooValue: 'erasdz',
barValue: 'eagrsdz',
};
const badVal = {
foo: 'erasdz',
bar: 'eagrsdz',
};
const goodValResult = t.validate(goodVal, t.dictionary(Keys, t.string));
const badValResult = t.validate(badVal, t.dictionary(Keys, t.string));
// @ts-ignore
console.log(PathReporter.report(goodValResult));
// @ts-ignore
console.log(PathReporter.report(badValResult));
Hi, I have a similar case, and tried @OliverJAsh example but it just doesn't work, I have a composited class with enums, arrays, and other objects inside that I want to validate but I don't know how to approach this. I've to say that I'm quite new to TS so I'm bit lost here. I would appreciate some guidance towards some documentation where I can understand the matter.
@alvico here's @OliverJAsh example upgraded to [email protected]
import * as t from 'io-ts'
import { PathReporter } from 'io-ts/lib/PathReporter'
// works with string enums (only)
export const createEnum = <E>(e: object, name: string): t.Type<E> => {
const keys = {}
Object.keys(e).forEach(k => {
keys[e[k]] = null
})
return t.keyof(keys, name) as any
}
//
// Usage
//
enum KeysT {
foo = 'fooValue',
bar = 'barValue'
}
const Keys = createEnum<KeysT>(KeysT, 'Keys')
const goodVal = {
fooValue: 'erasdz',
barValue: 'eagrsdz'
}
const badVal = {
foo: 'erasdz',
bar: 'eagrsdz'
}
const D = t.dictionary(Keys, t.string)
const goodValResult = D.decode(goodVal)
const badValResult = D.decode(badVal)
console.log(PathReporter.report(goodValResult))
/*
[ 'No errors!' ]
*/
console.log(PathReporter.report(badValResult))
/*
[ 'Invalid value "foo" supplied to : { [K in Keys]: string }/foo: Keys',
'Invalid value "bar" supplied to : { [K in Keys]: string }/bar: Keys' ]
*/
Why is this not part of the library? Enums are an extremely common use case and deserves first class support, IMO.
Any updates on this? Any chance it would get first class support?
For what it's worth, this is the solution/workaround I came up with for string enums:
const stringEnum = <T>(enumObj: T, enumName = "enum") => new Type<T[keyof T], string>(
enumName,
(u): u is T[keyof T] => Object.values(enumObj).includes(u),
(u, c) => Object.values(enumObj).includes(u) ? success(u as T[keyof T]) : failure(u, c),
a => a as unknown as string,
);
I have found very good solutions there and just want to link these issues
https://github.com/gcanti/io-ts/issues/216
there are kind of extensions of @johan13 idea
Guys, i wrote up nice solutions. It allows use enum in ts project in common way. Works fine with string enums and with digit enums.
function isConstEnum(e: any): boolean {
const values = Object.values(e);
const firstType = typeof values[0];
let result = false;
values.forEach(value => {
if (typeof value !== firstType) result = true;
});
return result;
}
function enumToArray<T>(e: T): [T[keyof T]] {
if (isConstEnum(e)) {
const keys = Object.keys(e).filter(k => typeof (e as any)[k as any] === 'number');
const values = keys.map(k => (e as any)[k as any]);
return values as any;
}
return Object.values(e) as any;
}
type IEnum = {
[key in string | number]: string | number;
};
export function validatorFromEnum<T extends IEnum>(e: T): t.Type<T[keyof T]> {
const arr = enumToArray(e);
if (arr.length < 2) throw new Error('Enum min length is 2');
if (typeof arr[0] === 'string') {
const reducedArr = arr.reduce((acc, value: string | number) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
acc[value] = null;
return acc;
}, {});
return t.keyof(reducedArr) as any;
}
const arrayOfLiteral: t.LiteralC<T[keyof T]>[] = [];
arr.forEach((value, index) => {
arrayOfLiteral[index] = t.literal(value);
});
const union = t.union(
arrayOfLiteral as [t.LiteralC<T[keyof T]>, t.LiteralC<T[keyof T]>, ...t.LiteralC<T[keyof T]>[]],
);
return union;
}
//Usage
`export enum UserGenders {
maleKey = 'male',
femaleKey = 'female',
}
const UserV = t.type(
{
gender: validatorFromEnum(UserGenders)
},
'User',
);
Most helpful comment
Why is this not part of the library? Enums are an extremely common use case and deserves first class support, IMO.